diff --git a/addons/account/account.py b/addons/account/account.py index 555d54c3ece..d2edea00d17 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -752,6 +752,7 @@ class account_journal(osv.osv): 'internal_account_id' : fields.many2one('account.account', 'Internal Transfers Account', select=1), 'cash_control' : fields.boolean('Cash Control', help='If you want the journal should be control at opening/closing, check this option'), 'analytic_journal_id':fields.many2one('account.analytic.journal','Analytic Journal', help="Journal for analytic entries"), + 'sequence': fields.integer('Sequence',help='Used to order Journals'), } _defaults = { @@ -759,13 +760,14 @@ class account_journal(osv.osv): 'with_last_closing_balance' : True, 'user_id': lambda self, cr, uid, context: uid, 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, + 'sequence': 1, } _sql_constraints = [ ('code_company_uniq', 'unique (code, company_id)', 'The code of the journal must be unique per company !'), ('name_company_uniq', 'unique (name, company_id)', 'The name of the journal must be unique per company !'), ] - _order = 'code' + _order = 'sequence,code' def _check_currency(self, cr, uid, ids, context=None): for journal in self.browse(cr, uid, ids, context=context): diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 6215103bba7..e4d0e9cf22c 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -360,6 +360,7 @@ account.journal + diff --git a/addons/account_anglo_saxon/test/anglo_saxon.yml b/addons/account_anglo_saxon/test/anglo_saxon.yml index ca082aea97c..b3588ad6621 100644 --- a/addons/account_anglo_saxon/test/anglo_saxon.yml +++ b/addons/account_anglo_saxon/test/anglo_saxon.yml @@ -212,8 +212,9 @@ product_uom_qty: 1.0 product_uom: product.product_uom_unit location_dest_id: stock.stock_location_customers - move_type: direct + invoice_state: 2binvoiced invoice_state: 2binvoiced + move_type: direct picking_type_id: stock.picking_type_out - I need to check the availability of the product, So I make my picking order for processing later. diff --git a/addons/account_anglo_saxon/test/anglo_saxon_avg_fifo.yml b/addons/account_anglo_saxon/test/anglo_saxon_avg_fifo.yml index b6c5d57d37f..f819fcdae7c 100644 --- a/addons/account_anglo_saxon/test/anglo_saxon_avg_fifo.yml +++ b/addons/account_anglo_saxon/test/anglo_saxon_avg_fifo.yml @@ -214,6 +214,7 @@ product_id: product_fifo_anglo_saxon product_uom_qty: 1.0 location_dest_id: stock.stock_location_customers + invoice_state: 2binvoiced invoice_state: 2binvoiced move_type: direct picking_type_id: stock.picking_type_out diff --git a/addons/barcodes/__init__.py b/addons/barcodes/__init__.py new file mode 100644 index 00000000000..16d15ce9bfe --- /dev/null +++ b/addons/barcodes/__init__.py @@ -0,0 +1,2 @@ +import barcodes + diff --git a/addons/barcodes/__openerp__.py b/addons/barcodes/__openerp__.py new file mode 100644 index 00000000000..b5300bdd660 --- /dev/null +++ b/addons/barcodes/__openerp__.py @@ -0,0 +1,31 @@ +{ + 'name': 'Barcodes', + 'version': '1.0', + 'category': '', + 'sequence': 6, + 'summary': 'Barcode Nomenclatures Setup', + 'description': """ + +======================= + +This module defines barcode nomenclatures whose rules identify e.g. products, locations. +It contains the following features: +- Barcode patterns to identify barcodes containing a numerical value (e.g. weight, price) +- Definitin of barcode aliases that allow to identify the same product with different barcodes +- Unlimited barcode patterns and definitions. +- Barcode EAN13 encoding supported +""", + 'author': 'OpenERP SA', + 'depends': ['web'], + 'website': '', + 'data': [ + 'data/barcodes_data.xml', + 'barcodes_view.xml', + 'security/ir.model.access.csv', + 'views/templates.xml', + ], + 'installable': True, + 'auto_install': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/barcodes/barcodes.py b/addons/barcodes/barcodes.py new file mode 100644 index 00000000000..ad72084bc03 --- /dev/null +++ b/addons/barcodes/barcodes.py @@ -0,0 +1,177 @@ +import logging +import re + +import openerp +from openerp import tools, models, fields, api +from openerp.osv import fields, osv +from openerp.tools.translate import _ +from openerp.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + +class barcode_nomenclature(osv.osv): + _name = 'barcode.nomenclature' + _columns = { + 'name': fields.char('Nomenclature Name', size=32, required=True, help='An internal identification of the barcode nomenclature'), + 'rule_ids': fields.one2many('barcode.rule','barcode_nomenclature_id','Rules', help='The list of barcode rules'), + 'strict_ean': fields.boolean('Use strict EAN13', + help='Many barcode scanners strip the leading zero of EAN13 barcodes. By using strict EAN13, we consider the scanned barcode directly. Otherwise, we prepend scanned barcodes of length 12 by a zero before looking for the associated item.') + } + + _defaults = { + 'strict_ean': False, + } + + def use_strict_ean(self): + return self.strict_ean + + # returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string + def ean_checksum(self, ean): + code = list(ean) + if len(code) != 13: + return -1 + + oddsum = evensum = total = 0 + code = code[:-1] # Remove checksum + for i in range(len(code)): + if i % 2 == 0: + evensum += int(code[i]) + else: + oddsum += int(code[i]) + total = oddsum * 3 + evensum + return int((10 - total % 10) % 10) + + # returns true if the barcode is a valid EAN barcode + def check_ean(self, ean): + return re.match("^\d+$", ean) and self.ean_checksum(ean) == int(ean[len(ean)-1]) + + # Returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. + def sanitize_ean(self, ean): + ean = ean[0:13] + ean = ean + (13-len(ean))*'0' + return ean[0:12] + str(self.ean_checksum(ean)) + + # Attempts to interpret an barcode (string encoding a barcode) + # It will return an object containing various information about the barcode. + # most importantly : + # - code : the barcode + # - type : the type of the barcode: + # - value : if the id encodes a numerical value, it will be put there + # - base_code : the barcode code with all the encoding parts set to zero; the one put on + # the product in the backend + def parse_barcode(self, barcode): + parsed_result = { + 'encoding': '', + 'type': 'error', + 'code': barcode, + 'base_code': barcode, + 'value': 0} + + # Checks if barcode matches the pattern + # Additionnaly retrieves the optional numerical content in barcode + # Returns an object containing: + # - value: the numerical value encoded in the barcode (0 if no value encoded) + # - base_code: the barcode in which numerical content is replaced by 0's + # - match: boolean + def match_pattern(barcode, pattern): + match = { + "value": 0, + "base_code": barcode, + "match": False, + } + + barcode = barcode.replace("\\", "\\\\").replace("{", '\{').replace("}", "\}").replace(".", "\.") + numerical_content = re.search("[{][N]*[D]*[}]", pattern) # look for numerical content in pattern + + if numerical_content: # the pattern encodes a numerical content + num_start = numerical_content.start() # start index of numerical content + num_end = numerical_content.end() # end index of numerical content + value_string = barcode[num_start:num_end-2] # numerical content in barcode + + whole_part_match = re.search("[{][N]*[D}]", numerical_content.group()) # looks for whole part of numerical content + decimal_part_match = re.search("[{N][D]*[}]", numerical_content.group()) # looks for decimal part + whole_part = value_string[:whole_part_match.end()-2] # retrieve whole part of numerical content in barcode + decimal_part = "0." + value_string[decimal_part_match.start():decimal_part_match.end()-1] # retrieve decimal part + if whole_part == '': + whole_part = '0' + match['value'] = int(whole_part) + float(decimal_part) + + match['base_code'] = barcode[:num_start] + (num_end-num_start-2)*"0" + barcode[num_end-2:] # replace numerical content by 0's in barcode + match['base_code'] = match['base_code'].replace("\\\\", "\\").replace("\{", "{").replace("\}","}").replace("\.",".") + pattern = pattern[:num_start] + (num_end-num_start-2)*"0" + pattern[num_end:] # replace numerical content by 0's in pattern to match + + match['match'] = re.match(pattern, match['base_code'][:len(pattern)]) + + return match + + + rules = [] + for rule in self.rule_ids: + rules.append({'type': rule.type, 'encoding': rule.encoding, 'sequence': rule.sequence, 'pattern': rule.pattern, 'alias': rule.alias}) + + # If the nomenclature does not use strict EAN, prepend the barcode with a 0 if it seems + # that it has been striped by the barcode scanner, when trying to match an EAN13 rule + prepend_zero = False + if not self.strict_ean and len(barcode) == 12 and self.check_ean("0"+barcode): + prepend_zero = True + + for rule in rules: + cur_barcode = barcode + if prepend_zero and rule['encoding'] == "ean13": + cur_barcode = '0'+cur_barcode + + match = match_pattern(cur_barcode, rule['pattern']) + if match['match']: + if rule['type'] == 'alias': + barcode = rule['alias'] + parsed_result['code'] = barcode + else: + parsed_result['encoding'] = rule['encoding'] + parsed_result['type'] = rule['type'] + parsed_result['value'] = match['value'] + parsed_result['code'] = cur_barcode + if rule['encoding'] == "ean13": + parsed_result['base_code'] = self.sanitize_ean(match['base_code']) + else: + parsed_result['base_code'] = match['base_code'] + return parsed_result + + return parsed_result + +class barcode_rule(models.Model): + _name = 'barcode.rule' + _order = 'sequence asc' + + @api.model + def _get_type_selection(self): + return [('alias','Alias'),('product','Unit Product')] + + _columns = { + 'name': fields.char('Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule'), + 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcode Nomenclature'), + 'sequence': fields.integer('Sequence', help='Used to order rules such that rules with a smaller sequence match first'), + 'encoding': fields.selection([('any','Any'),('ean13','EAN-13')],'Encoding',required=True,help='This rule will apply only if the barcode is encoded with the specified encoding'), + 'type': fields.selection('_get_type_selection','Type', required=True), + 'pattern': fields.char('Barcode Pattern', size=32, help="The barcode matching pattern"), + 'alias': fields.char('Alias',size=32,help='The matched pattern will alias to this barcode',required=True), + } + + _defaults = { + 'type': 'product', + 'pattern': '*', + 'encoding': 'any', + 'alias': "0", + } + + @api.one + @api.constrains('pattern') + def _check_pattern(self): + p = self.pattern.replace("\\\\", "X").replace("\{", "X").replace("\}", "X") + findall = re.findall("[{]|[}]", p) # p does not contain escaped { or } + if len(findall) == 2: + if not re.search("[{][N]*[D]*[}]", p): + raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": braces can only contain N's followed by D's.")) + elif re.search("[{][}]", p): + raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": empty braces.")) + elif len(findall) != 0: + raise ValidationError(_("There is a syntax error in the barcode pattern ") + self.pattern + _(": a rule can only contain one pair of braces.")) diff --git a/addons/barcodes/barcodes_view.xml b/addons/barcodes/barcodes_view.xml new file mode 100644 index 00000000000..e3089837674 --- /dev/null +++ b/addons/barcodes/barcodes_view.xml @@ -0,0 +1,86 @@ + + + + + + Barcode Nomenclatures + barcode.nomenclature + +
+ + + + + +
+

+ Barcodes Nomenclatures define how barcodes are recognized and categorized. + When a barcode is scanned it is associated to the first rule with a matching + pattern. The pattern syntax is that of regular expression, and a barcode is matched + if the regular expression matches a prefix of the barcode. +

+ Patterns can also define how numerical values, such as weight or price, can be + encoded into the barcode. They are indicated by a {NNN} where the N's + define where the number's digits are encoded. Floats are also supported with the + decimals indicated with D's, such as {NNNDD}. In these cases, + the barcodes on the associated entites must have the value part set to + zeroes. +

+
+ + + + + + + + + +
+
+
+
+ + + Barcode Nomenclatures + barcode.nomenclature + + + + + + + + + Barcode Nomenclatures + ir.actions.act_window + barcode.nomenclature + form + tree,form + +

+ Click to add a Barcode Nomenclature . +

+ A barcode nomenclature defines how the point of sale identify and interprets barcodes +

+
+
+ + + Barcode Rule + barcode.rule + +
+ + + + + + + + +
+
+
+
+
diff --git a/addons/barcodes/data/barcodes_data.xml b/addons/barcodes/data/barcodes_data.xml new file mode 100644 index 00000000000..2c8b711ba67 --- /dev/null +++ b/addons/barcodes/data/barcodes_data.xml @@ -0,0 +1,17 @@ + + + + + Default Nomenclature + + + + Product Barcodes + + 90 + product + any + * + + + diff --git a/addons/barcodes/doc/index.rst b/addons/barcodes/doc/index.rst new file mode 100644 index 00000000000..6cdacdc3a11 --- /dev/null +++ b/addons/barcodes/doc/index.rst @@ -0,0 +1,75 @@ +Barcodes module documentation +============================== + +The Barcodes module defines barcode nomenclatures whose rules identify specific type +of items e.g. products, locations. It contains the following features: + +- Barcode patterns to identify barcodes containing a numerical value (e.g. weight, price) +- Definitin of barcode aliases that allow to identify the same product with different barcodes +- Unlimited barcode patterns and definitions, +- Barcode EAN13 encoding supported. + +Barcode encodings +----------------- + +A barcode is an arbitrary long sequence of ASCII characters. An EAN-13 barcode is a 13 digit +barcode, whose 13th digit is the checksum. + +Simple barcodes and rules +------------------------- + +The default nomenclature assumes an EAN-13 encoding for product barcodes. It defines a rule +for Unit Products whose encoding is EAN-13, and whose pattern is '.', i.e. any barcode +matches this pattern. Scanning the barcode of a product, say '5410013101703', matches this rule. +The scanned item is thus identified as a Unit Product, and is retrieved from the product table. + +Note: the special character '.' in patterns is matched by any character. To explicitely specify +the '.' character in a pattern, escape it with '\'. The '\' character has to be escaped as well +('\\') to be explicitely specified. + +Let us now suppose that we identify other items with barcodes, say stock locations. We define a +new rule in the nomenclature with the corresponding type (in our example, the type is 'Location'), +and whose pattern is e.g. '414.', that is, any location barcode starts with '414'. Scanning a barcode +location, say '41401', matches this Location rule, and the corresponding location is retrieved from +the location table. + +Note: Rules have a sequence field which indicates the order the rules are evaluated (ASC). In our +previous examples, the Unit Product rule should have a larger sequence that then Location rule, +because we want the latter one to be evaluated first. + +Barcodes with numerical content +-------------------------------- + +Barcodes may encode numerical content, which is decoded by the barcodes module. To that purpose, +one have to define a new rule for barcodes with numerical content (e.g. barcodes for Weighted +Products). The numerical content in a pattern is specified between braces (special characters '{' and +'}'). The content of the braces must be a sequence of 'N's (representing the whole part of the numerical +content) followed by a sequence of 'D's (representing the decimal part of the numerical content). +For instance, let us define a new rule for Weighted Products whose pattern is '21.....{NNDDD}.'. Since +we assume EAN-13 encoding for product barcodes, the encoding of this rule should be EAN-13 as well. + +Let us now assume that we want to write a barcode for a given Weighted Product, say oranges. We first +have to define in product oranges a barcode that will match the Weighted Product rule. This barcode +must start with '21' and be a correct EAN-13 barcode (i.e. the 13th digit must be a correct checksum). +Moreover, all the numerical content must be set to a '0'. For instance, let us set the barcode to +'2100001000004'. + +We now want to write a barcode for 2.75kg of oranges. This barcode should be '2100001027506' (the +numerical content of this barcode is '02750', and the correct checksum is '6'). When scanned, this +barcode matches the Weighted Product rule (since is starts with '21'). The numerical content is extracted, +and replaced by a sequence of '0's. The correct checksum is then computed for the obtained barcode +('2100001000004') and the corresponding product (oranges) qgit is retrieved from product table. + +Note: the special characters '{' and '}' in patterns are used to identify numerical content. To +explicitely specify '{' or '}' in a pattern, they must be escaped. + + +Strict EAN-13 field of barcode nomenclatures +-------------------------------------------- + +Many barcode scanners strip the leading zero when scanning EAN-13 barcodes. Barcode nomenclatures +have a boolean field "Use strict EAN13". If False, when trying to match a scanned barcode with +a rule whose encoding is EAN-13, if the barcode is of length 12 and, by prepending it by a 0, +the last digit is the correct checksum, we automatically prepend the barcode by 0 and try to +find a match with this new barcode. If "Use strict EAN13" is set to True, we look for a pattern +matching the original, 12-digit long, barcode. diff --git a/addons/barcodes/security/ir.model.access.csv b/addons/barcodes/security/ir.model.access.csv new file mode 100644 index 00000000000..30ba7441424 --- /dev/null +++ b/addons/barcodes/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_barcode_nomenclature_user,barcode.nomenclature.user,model_barcode_nomenclature,base.group_user,1,0,0,0 +access_barcode_nomenclature_manager,barcode.nomenclature.manager,model_barcode_nomenclature,base.group_erp_manager,1,1,1,1 +access_barcode_rule_user,barcode.rule.user,model_barcode_rule,base.group_user,1,0,0,0 +access_barcode_rule_manager,barcode.rule.manager,model_barcode_rule,base.group_erp_manager,1,1,1,1 diff --git a/addons/barcodes/static/src/js/barcode_parser.js b/addons/barcodes/static/src/js/barcode_parser.js new file mode 100644 index 00000000000..3ce07be1755 --- /dev/null +++ b/addons/barcodes/static/src/js/barcode_parser.js @@ -0,0 +1,184 @@ +openerp.barcodes = function(instance) { + "use strict"; + + instance.barcodes = {}; + var module = instance.barcodes; + + // The BarcodeParser is used to detect what is the category + // of a barcode (product, partner, ...) and extract an encoded value + // (like weight, price, etc.) + module.BarcodeParser = instance.web.Class.extend({ + init: function(attributes) { + var self = this; + this.nomenclature_id = attributes.nomenclature_id; + this.loaded = this.load(); + }, + + // This loads the barcode nomenclature and barcode rules which are + // necessary to parse the barcodes. The BarcodeParser is operational + // only when those data have been loaded + load: function(){ + var self = this; + return new instance.web.Model('barcode.nomenclature') + .query(['name','rule_ids','strict_ean']) + .filter([['id','=',this.nomenclature_id[0]]]) + .first() + .then(function(nomenclature){ + self.nomenclature = nomenclature; + + return new instance.web.Model('barcode.rule') + .query(['name','sequence','type','encoding','pattern','alias']) + .filter([['barcode_nomenclature_id','=',self.nomenclature.id ]]) + .all() + }).then(function(rules){ + rules = rules.sort(function(a,b){ return a.sequence - b.sequence; }); + self.nomenclature.rules = rules; + }); + }, + + // resolves when the barcode parser is operational. + is_loaded: function() { + return self.loaded; + }, + + // returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string + ean_checksum: function(ean){ + var code = ean.split(''); + if(code.length !== 13){ + return -1; + } + var oddsum = 0, evensum = 0, total = 0; + code = code.reverse().splice(1); + for(var i = 0; i < code.length; i++){ + if(i % 2 == 0){ + oddsum += Number(code[i]); + }else{ + evensum += Number(code[i]); + } + } + total = oddsum * 3 + evensum; + return Number((10 - total % 10) % 10); + }, + + // returns true if the ean is a valid EAN barcode number by checking the control digit. + // ean must be a string + check_ean: function(ean){ + return /^\d+$/.test(ean) && this.ean_checksum(ean) === Number(ean[ean.length-1]); + }, + + // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. + sanitize_ean: function(ean){ + ean = ean.substr(0,13); + + for(var n = 0, count = (13 - ean.length); n < count; n++){ + ean = ean + '0'; + } + return ean.substr(0,12) + this.ean_checksum(ean); + }, + + // attempts to interpret a barcode (string encoding a barcode Code-128) + // it will return an object containing various information about the barcode. + // most importantly : + // - code : the barcode + // - type : the type of the barcode (e.g. alias, unit product, weighted product...) + // + // - value : if the barcode encodes a numerical value, it will be put there + // - base_code : the barcode with all the encoding parts set to zero; the one put on + // the product in the backend + parse_barcode: function(barcode){ + var self = this; + var parsed_result = { + encoding: '', + type:'error', + code:barcode, + base_code: barcode, + value: 0, + }; + if (!self.nomenclature) { + return parsed_result; + } + + // Checks if barcode matches the pattern + // Additionnaly retrieves the optional numerical content in barcode + // Returns an object containing: + // - value: the numerical value encoded in the barcode (0 if no value encoded) + // - base_code: the barcode in which numerical content is replaced by 0's + // - match: boolean + function match_pattern(barcode, pattern){ + var match = { + value: 0, + base_code: barcode, + match: false, + }; + barcode = barcode.replace("\\", "\\\\").replace("{", '\{').replace("}", "\}").replace(".", "\."); + + var numerical_content = pattern.match(/[{][N]*[D]*[}]/); // look for numerical content in pattern + var base_pattern = pattern; + if(numerical_content){ // the pattern encodes a numerical content + var num_start = numerical_content.index; // start index of numerical content + var num_length = numerical_content[0].length; // length of numerical content + var value_string = barcode.substr(num_start, num_length-2); // numerical content in barcode + var whole_part_match = numerical_content[0].match("[{][N]*[D}]"); // looks for whole part of numerical content + var decimal_part_match = numerical_content[0].match("[{N][D]*[}]"); // looks for decimal part + var whole_part = value_string.substr(0, whole_part_match.index+whole_part_match[0].length-2); // retrieve whole part of numerical content in barcode + var decimal_part = "0." + value_string.substr(decimal_part_match.index, decimal_part_match[0].length-1); // retrieve decimal part + if (whole_part === ''){ + whole_part = '0'; + } + match['value'] = parseInt(whole_part) + parseFloat(decimal_part); + + // replace numerical content by 0's in barcode and pattern + match['base_code'] = barcode.substr(0,num_start); + var base_pattern = pattern.substr(0,num_start); + for(var i=0;i<(num_length-2);i++) { + match['base_code'] += "0"; + base_pattern += "0"; + } + match['base_code'] += barcode.substr(num_start+num_length-2,barcode.length-1); + base_pattern += pattern.substr(num_start+num_length,pattern.length-1); + + match['base_code'] = match['base_code'].replace("\\\\", "\\").replace("\{", "{").replace("\}","}").replace("\.","."); + } + + match['match'] = match['base_code'].substr(0,base_pattern.length).match(base_pattern); + return match; + } + + // If the nomenclature does not use strict EAN, prepend the barcode with a 0 if it seems + // that it has been striped by the barcode scanner, when trying to match an EAN13 rule + var prepend_zero = false; + if(!self.strict_ean && barcode.length === 12 && self.check_ean("0"+barcode)){ + prepend_zero = true; + } + var rules = self.nomenclature.rules; + for (var i = 0; i < rules.length; i++) { + var cur_barcode = barcode; + if (prepend_zero && rules[i].encoding == 'ean13'){ + cur_barcode = '0'+cur_barcode; + } + var match = match_pattern(cur_barcode,rules[i].pattern); + if (match.match) { + if(rules[i].type === 'alias') { + barcode = rules[i].alias; + parsed_result.code = barcode; + parsed_result.type = 'alias'; + } + else { + parsed_result.encoding = rules[i].encoding; + parsed_result.type = rules[i].type; + parsed_result.value = match.value + parsed_result.code = cur_barcode; + if (rules[i].encoding === "ean13"){ + parsed_result.base_code = this.sanitize_ean(match.base_code); + } + else{ + parsed_result.base_code = match.base_code; + } + return parsed_result; + } + } + } + return parsed_result; + }, + }); +} diff --git a/addons/barcodes/views/templates.xml b/addons/barcodes/views/templates.xml new file mode 100644 index 00000000000..9e255e57290 --- /dev/null +++ b/addons/barcodes/views/templates.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index ccf472c72a3..40a98b1888c 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -48,9 +48,10 @@ Main Features """, 'author': 'OpenERP SA', 'images': ['images/pos_touch_screen.jpeg', 'images/pos_session.jpeg', 'images/pos_analysis.jpeg','images/sale_order_pos.jpeg','images/product_pos.jpeg'], - 'depends': ['sale_stock'], + 'depends': ['sale_stock', 'barcodes'], 'data': [ 'data/report_paperformat.xml', + 'data/default_barcode_patterns.xml', 'security/point_of_sale_security.xml', 'security/ir.model.access.csv', 'wizard/pos_box.xml', diff --git a/addons/point_of_sale/data/default_barcode_patterns.xml b/addons/point_of_sale/data/default_barcode_patterns.xml new file mode 100644 index 00000000000..1efcf941597 --- /dev/null +++ b/addons/point_of_sale/data/default_barcode_patterns.xml @@ -0,0 +1,40 @@ + + + + + Cashier Barcodes + + 50 + cashier + any + 041 + + + + Customer Barcodes + + 40 + client + any + 042 + + + + Discount Barcodes + + 20 + discount + any + 22{NN} + + + + Price Barcodes 2 Decimals + + 14 + price + ean13 + 23.....{NNNDD} + + + diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 5b67bdc9c1c..d30d754ff24 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -21,8 +21,10 @@ import logging import time +import uuid +import sets -from openerp import tools +from openerp import tools, models from openerp.osv import fields, osv from openerp.tools.translate import _ from openerp.exceptions import Warning @@ -63,8 +65,6 @@ class pos_config(osv.osv): domain=[('type', '=', 'sale')], help="Accounting journal used to post sales entries."), 'currency_id' : fields.function(_get_currency, type="many2one", string="Currency", relation="res.currency"), - 'iface_self_checkout' : fields.boolean('Self Checkout Mode', # FIXME : this field is obsolete - help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, Odoo uses the normal cashier mode by default."), 'iface_cashdrawer' : fields.boolean('Cashdrawer', help="Automatically open the cashdrawer"), 'iface_payment_terminal' : fields.boolean('Payment Terminal', help="Enables Payment Terminal integration"), 'iface_electronic_scale' : fields.boolean('Electronic Scale', help="Enables Electronic Scale integration"), @@ -74,11 +74,15 @@ class pos_config(osv.osv): 'iface_invoicing': fields.boolean('Invoicing',help='Enables invoice generation from the Point of Sale'), 'iface_big_scrollbars': fields.boolean('Large Scrollbars',help='For imprecise industrial touchscreens'), 'iface_fullscreen': fields.boolean('Fullscreen', help='Display the Point of Sale in full screen mode'), + 'iface_print_auto': fields.boolean('Automatic Receipt Printing', help='The receipt will automatically be printed at the end of each order'), + 'iface_precompute_cash': fields.boolean('Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount'), + 'iface_tax_included': fields.boolean('Include Taxes in Prices', help='The displayed prices will always include all taxes, even if the taxes have been setup differently'), 'receipt_header': fields.text('Receipt Header',help="A short text that will be inserted as a header in the printed receipt"), 'receipt_footer': fields.text('Receipt Footer',help="A short text that will be inserted as a footer in the printed receipt"), 'proxy_ip': fields.char('IP Address', help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty', size=45), 'state' : fields.selection(POS_CONFIG_STATE, 'Status', required=True, readonly=True, copy=False), + 'uuid' : fields.char('uuid', readonly=True, help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data'), 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, help="This sequence is automatically created by Odoo but you can change it "\ "to customize the reference numbers of your orders.", copy=False), @@ -86,12 +90,9 @@ class pos_config(osv.osv): 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), 'pricelist_id': fields.many2one('product.pricelist','Pricelist', required=True), 'company_id': fields.many2one('res.company', 'Company', required=True), - 'barcode_product': fields.char('Product Barcodes', size=64, help='The pattern that identifies product barcodes'), - 'barcode_cashier': fields.char('Cashier Barcodes', size=64, help='The pattern that identifies cashier login barcodes'), - 'barcode_customer': fields.char('Customer Barcodes',size=64, help='The pattern that identifies customer\'s client card barcodes'), - 'barcode_price': fields.char('Price Barcodes', size=64, help='The pattern that identifies a product with a barcode encoded price'), - 'barcode_weight': fields.char('Weight Barcodes', size=64, help='The pattern that identifies a product with a barcode encoded weight'), - 'barcode_discount': fields.char('Discount Barcodes', size=64, help='The pattern that identifies a product with a barcode encoded discount'), + 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcodes', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers', required=True), + 'group_pos_manager_id': fields.many2one('res.groups','Point of Sale Manager Group', help='This field is there to pass the id of the pos manager group to the point of sale client'), + 'group_pos_user_id': fields.many2one('res.groups','Point of Sale User Group', help='This field is there to pass the id of the pos user group to the point of sale client'), } def _check_cash_control(self, cr, uid, ids, context=None): @@ -164,22 +165,41 @@ class pos_config(osv.osv): def _get_default_company(self, cr, uid, context=None): company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) + print company_id return company_id + def _get_default_nomenclature(self, cr, uid, context=None): + nom_obj = self.pool.get('barcode.nomenclature') + res = nom_obj.search(cr, uid, [], limit=1, context=context) + return res and res[0] or False + + def _get_group_pos_manager(self, cr, uid, context=None): + group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_manager') + if group: + return group[1] + else: + return False + + def _get_group_pos_user(self, cr, uid, context=None): + group = self.pool.get('ir.model.data').get_object_reference(cr,uid,'point_of_sale','group_pos_user') + if group: + return group[1] + else: + return False + _defaults = { + 'uuid' : lambda self, cr, uid, context={}: str(uuid.uuid4()), 'state' : POS_CONFIG_STATE[0][0], 'journal_id': _default_sale_journal, 'group_by' : True, 'pricelist_id': _default_pricelist, 'iface_invoicing': True, + 'iface_print_auto': True, 'stock_location_id': _get_default_location, 'company_id': _get_default_company, - 'barcode_product': '*', - 'barcode_cashier': '041*', - 'barcode_customer':'042*', - 'barcode_weight': '21xxxxxNNDDD', - 'barcode_discount':'22xxxxxxxxNN', - 'barcode_price': '23xxxxxNNNDD', + 'barcode_nomenclature_id': _get_default_nomenclature, + 'group_pos_manager_id': _get_group_pos_manager, + 'group_pos_user_id': _get_group_pos_user, } def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): @@ -1338,7 +1358,7 @@ class ean_wizard(osv.osv_memory): ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern) m = context.get('active_model') m_id = context.get('active_id') - self.pool[m].write(cr,uid,[m_id],{'ean13':ean13}) + self.pool[m].write(cr,uid,[m_id],{'barcode':ean13}) return { 'type' : 'ir.actions.act_window_close' } class pos_category(osv.osv): @@ -1443,6 +1463,18 @@ class res_partner(osv.osv): return partner_id +class barcode_rule(models.Model): + _inherit = 'barcode.rule' + def _get_type_selection(self): + types = sets.Set(super(barcode_rule,self)._get_type_selection()) + types.update([ + ('weight','Weighted Product'), + ('price','Priced Product'), + ('discount','Discounted Product'), + ('client','Client'), + ('cashier','Cashier') + ]) + return list(types) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/point_of_sale/point_of_sale_data.xml b/addons/point_of_sale/point_of_sale_data.xml index 1013c3ce3ca..8124f69d5a7 100644 --- a/addons/point_of_sale/point_of_sale_data.xml +++ b/addons/point_of_sale/point_of_sale_data.xml @@ -30,6 +30,7 @@ Main + diff --git a/addons/point_of_sale/point_of_sale_demo.xml b/addons/point_of_sale/point_of_sale_demo.xml index ea6d62449f8..aa17e053e6d 100644 --- a/addons/point_of_sale/point_of_sale_demo.xml +++ b/addons/point_of_sale/point_of_sale_demo.xml @@ -1,37 +1,37 @@ - - 0420100000005 - 0420200000004 - 0420300000003 - 0420400000002 - 0420500000001 - 0420600000000 - 0420700000009 - 0420800000008 - 0420900000007 - 0421000000003 - 0421100000002 - 0421200000001 - 0421300000000 - 0421400000009 - 0421500000008 - 0421600000007 - 0421700000006 - 0421800000005 - 0421900000004 - 0422000000000 - 0422100000009 + + 0420100000005 + 0420200000004 + 0420300000003 + 0420400000002 + 0420500000001 + 0420600000000 + 0420700000009 + 0420800000008 + 0420900000007 + 0421000000003 + 0421100000002 + 0421200000001 + 0421300000000 + 0421400000009 + 0421500000008 + 0421600000007 + 0421700000006 + 0421800000005 + 0421900000004 + 0422000000000 + 0422100000009 - 0410100000006 + 0410100000006 - 0410100000006 + 0410100000006 @@ -191,7 +191,7 @@ 1.98 Boni Oranges True - 2100002000003 + 2100002000003 @@ -212,7 +212,7 @@ True 1.98 Lemon - 2301000000006 + 2301000000006 True @@ -297,7 +297,7 @@ 5.10 Peaches True - 2300001000008 + 2300001000008 @@ -373,7 +373,7 @@ 1.28 Onions True - 2100001000004 + 2100001000004 @@ -435,7 +435,7 @@ True 1.49 Coca-Cola Regular 1L - 5449000054227 + 5449000054227 @@ -457,7 +457,7 @@ True 0.51 Coca-Cola Regular 33cl - 5449000000996 + 5449000000996 @@ -722,7 +722,7 @@ True 0.44 - 5449000111715 + 5449000111715 Chaudfontaine 50cl @@ -837,7 +837,7 @@ True 0.77 - 5410228142027 + 5410228142027 Jupiler 33cl @@ -896,7 +896,7 @@ True 0.83 Belle-Vue Kriek 25cl - 5410228193449 + 5410228193449 @@ -905,7 +905,7 @@ True 1.00 Leffe Brune 33cl - 5410228142164 + 5410228142164 @@ -913,7 +913,7 @@ True 1.00 Leffe Blonde 33cl - 5410228142218 + 5410228142218 diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index d1edbdf43cc..8e6ea1df174 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -553,7 +553,7 @@ product.template - + - @@ -338,7 +334,7 @@
Barcode - +
Tax ID @@ -380,10 +376,10 @@
Barcode - - + + - + N/A
@@ -458,7 +454,7 @@
- +
Please select a payment method. @@ -488,17 +484,17 @@ - + - + - + - + @@ -508,11 +504,11 @@ - + - - + + @@ -556,13 +552,14 @@
-
+
+
- +
@@ -592,25 +589,27 @@
+
+ Customer +
-
- Invoice -
+ +
+ Invoice +
+
Open Cashbox
-
- - -
-
-
- -
- +
+
+
+
+ +
@@ -661,19 +660,24 @@
- + + + +
-
--------------------------------
-
Served by
+
+
--------------------------------
+
Served by
+


-
+
@@ -725,7 +729,7 @@ -------- - +
        TOTAL
@@ -762,16 +766,28 @@
+ + Total Taxes + +
+ diff --git a/addons/pos_restaurant/views/templates.xml b/addons/pos_restaurant/views/templates.xml index 10c269be86f..b549c7da970 100644 --- a/addons/pos_restaurant/views/templates.xml +++ b/addons/pos_restaurant/views/templates.xml @@ -12,9 +12,11 @@ diff --git a/addons/product/product.py b/addons/product/product.py index 589ff9e5cb7..fa35591c0b4 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -55,18 +55,6 @@ def ean_checksum(eancode): check = int(10 - math.ceil(total % 10.0)) %10 return check -def check_ean(eancode): - """returns True if eancode is a valid ean13 string, or null""" - if not eancode: - return True - if len(eancode) != 13: - return False - try: - int(eancode) - except: - return False - return ean_checksum(eancode) == int(eancode[-1]) - def sanitize_ean13(ean13): """Creates and returns a valid ean13 from an invalid one""" if not ean13: @@ -494,6 +482,7 @@ class product_template(osv.osv): _columns = { 'name': fields.char('Name', required=True, translate=True, select=True), + 'sequence': fields.integer('Sequence', help='Gives the sequence order when displaying a product list'), 'product_manager': fields.many2one('res.users','Product Manager'), 'description': fields.text('Description',translate=True, help="A precise description of the Product, used only for internal information purposes."), @@ -571,7 +560,7 @@ class product_template(osv.osv): 'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'), # related to display product product information if is_product_variant - 'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'), + 'barcode': fields.related('product_variant_ids', 'barcode', type='char', string='Barcode', oldname='ean13'), 'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'), } @@ -713,8 +702,8 @@ class product_template(osv.osv): # TODO: this is needed to set given values to first variant after creation # these fields should be moved to product as lead to confusion related_vals = {} - if vals.get('ean13'): - related_vals['ean13'] = vals['ean13'] + if vals.get('barcode'): + related_vals['barcode'] = vals['barcode'] if vals.get('default_code'): related_vals['default_code'] = vals['default_code'] if related_vals: @@ -760,6 +749,7 @@ class product_template(osv.osv): 'categ_id' : _default_category, 'type' : 'consu', 'active': True, + 'sequence': 1, } def _check_uom(self, cursor, user, ids, context=None): @@ -932,7 +922,7 @@ class product_product(osv.osv): 'default_code' : fields.char('Internal Reference', select=True), 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."), 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True, auto_join=True), - 'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."), + 'barcode': fields.char('Barcode', help="International Article Number used for product identification.", oldname='ean13'), 'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={ 'product.template': (_get_name_template_ids, ['name'], 10), 'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10), @@ -989,14 +979,6 @@ class product_product(osv.osv): return {'value': {'uom_po_id': uom_id}} return False - def _check_ean_key(self, cr, uid, ids, context=None): - for product in self.read(cr, uid, ids, ['ean13'], context=context): - if not check_ean(product['ean13']): - return False - return True - - _constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])] - def on_order(self, cr, uid, ids, orderline, quantity): pass @@ -1062,7 +1044,7 @@ class product_product(osv.osv): if operator in positive_operators: ids = self.search(cr, user, [('default_code','=',name)]+ args, limit=limit, context=context) if not ids: - ids = self.search(cr, user, [('ean13','=',name)]+ args, limit=limit, context=context) + ids = self.search(cr, user, [('barcode','=',name)]+ args, limit=limit, context=context) if not ids and operator not in expression.NEGATIVE_TERM_OPERATORS: # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal # on a database with thousands of matching products, due to the huge merge+unique needed for the @@ -1176,7 +1158,7 @@ class product_product(osv.osv): class product_packaging(osv.osv): _name = "product.packaging" _description = "Packaging" - _rec_name = 'ean' + _rec_name = 'barcode' _order = 'sequence' _columns = { 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of packaging."), @@ -1189,26 +1171,18 @@ class product_packaging(osv.osv): 'rows' : fields.integer('Number of Layers', required=True, help='The number of layers on a pallet or box'), 'product_tmpl_id' : fields.many2one('product.template', 'Product', select=1, ondelete='cascade', required=True), - 'ean' : fields.char('EAN', size=14, help="The EAN code of the package unit."), + 'barcode' : fields.char('Barcode', help="The Barcode of the package unit.", oldname="ean"), 'code' : fields.char('Code', help="The code of the transport unit."), 'weight': fields.float('Total Package Weight', help='The weight of a full package, pallet or box.'), } - def _check_ean_key(self, cr, uid, ids, context=None): - for pack in self.browse(cr, uid, ids, context=context): - if not check_ean(pack.ean): - return False - return True - - _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])] - def name_get(self, cr, uid, ids, context=None): if not len(ids): return [] res = [] for pckg in self.browse(cr, uid, ids, context=context): - p_name = pckg.ean and '[' + pckg.ean + '] ' or '' + p_name = pckg.barcode and '[' + pckg.barcode + '] ' or '' p_name += pckg.ul.name res.append((pckg.id,p_name)) return res diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index f07769bb6f0..7de859bc358 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -246,7 +246,7 @@ Product Template - + @@ -343,7 +343,7 @@ - + @@ -361,7 +361,7 @@ Product Variant - + @@ -735,7 +735,7 @@ - + @@ -765,7 +765,7 @@ - + @@ -788,7 +788,7 @@
- + diff --git a/addons/product/report/product_label.xml b/addons/product/report/product_label.xml index 00d587dc782..6c9f657d6dc 100644 --- a/addons/product/report/product_label.xml +++ b/addons/product/report/product_label.xml @@ -5,7 +5,7 @@ - + diff --git a/addons/product/report/product_label.xsl b/addons/product/report/product_label.xsl index 2e062222bbd..01c5ca4059e 100644 --- a/addons/product/report/product_label.xsl +++ b/addons/product/report/product_label.xsl @@ -74,7 +74,7 @@ - + , diff --git a/addons/product_expiry/product_expiry_demo.xml b/addons/product_expiry/product_expiry_demo.xml index 5c1c2e67ca6..a36a72a3b00 100644 --- a/addons/product_expiry/product_expiry_demo.xml +++ b/addons/product_expiry/product_expiry_demo.xml @@ -15,7 +15,7 @@ 6 5 JKB - 3307630302014 + 3307630302014 @@ -27,7 +27,7 @@ 6 4 - + 10 @@ -44,7 +44,7 @@ 20 19 FKS - 3307631101876 + 3307631101876 @@ -55,7 +55,7 @@ 6 4 - + 10 @@ -72,7 +72,7 @@ 20 19 PKB - 3307631102323 + 3307631102323 @@ -83,7 +83,7 @@ 6 4 - + 10 @@ -100,7 +100,7 @@ 20 19 LEX - 3307630302045 + 3307630302045 @@ -111,7 +111,7 @@ 6 4 - + 10 diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 2375063931f..89b6c3680f6 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -263,15 +263,15 @@ class sale_order_line(osv.osv): q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom) # qty = qty - qty % q + q if qty and (q and not (qty % q) == 0): - ean = pack.ean or _('(n/a)') + barcode = pack.barcode or _('(n/a)') qty_pack = pack.qty type_ul = pack.ul if not warning_msgs: warn_msg = _("You selected a quantity of %d Units.\n" "But it's not compatible with the selected packaging.\n" "Here is a proposition of quantities according to the packaging:\n" - "EAN: %s Quantity: %s Type of ul: %s") % \ - (qty, ean, qty_pack, type_ul.name) + "Barcode: %s Quantity: %s Type of ul: %s") % \ + (qty, barcode, qty_pack, type_ul.name) warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n" warning = { 'title': _('Configuration Error!'), diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index ee1df5062b3..35aa355f420 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -52,7 +52,7 @@ Dashboard / Reports for Warehouse Management will include: """, 'website': 'https://www.odoo.com/page/warehouse', 'images': ['images/stock_forecast_report.png', 'images/delivery_orders.jpeg', 'images/inventory_analysis.jpeg','images/location.jpeg','images/moves_analysis.jpeg','images/physical_inventories.jpeg'], - 'depends': ['product', 'procurement', 'board', 'web_kanban_gauge', 'web_kanban_sparkline'], + 'depends': ['product', 'procurement', 'board', 'web_kanban_gauge', 'web_kanban_sparkline', 'barcodes'], 'category': 'Warehouse Management', 'sequence': 16, 'demo': [ @@ -66,6 +66,7 @@ Dashboard / Reports for Warehouse Management will include: 'stock_location_demo_cpu3.yml', ], 'data': [ + 'data/default_barcode_patterns.xml', 'security/stock_security.xml', 'security/ir.model.access.csv', 'stock_data.xml', diff --git a/addons/stock/data/default_barcode_patterns.xml b/addons/stock/data/default_barcode_patterns.xml new file mode 100644 index 00000000000..150cb4c94a1 --- /dev/null +++ b/addons/stock/data/default_barcode_patterns.xml @@ -0,0 +1,40 @@ + + + + + Weight Barcodes 3 Decimals + + 36 + weight + ean13 + 21.....{NNDDD} + + + + Package barcodes + + 70 + package + any + PACK + + + + Lot barcodes + + 80 + lot + any + 10 + + + + Location barcodes + + 60 + location + any + 414 + + + diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index b05ab25d3cf..cbe1d652e4d 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -75,3 +75,7 @@ access_stock_move_operation_link_user,stock.move.operation.link user,model_stock access_stock_move_operation_link_all,stock.move.operation.link all users,model_stock_move_operation_link,base.group_user,1,0,0,0 access_product_price_history_stock_user,prices.history stock user,product.model_product_price_history,stock.group_stock_user,1,0,1,0 access_product_price_history_stock_manager,prices.history stock manager,product.model_product_price_history,stock.group_stock_manager,1,1,1,1 +access_barcode_nomenclature_stock_user,barcode.nomenclature.stock.user,barcodes.model_barcode_nomenclature,stock.group_stock_user,1,0,0,0 +access_barcode_nomenclature_stock_manager,barcode.nomenclature.stock.manager,barcodes.model_barcode_nomenclature,stock.group_stock_manager,1,1,1,1 +access_barcode_rule_stock_user,barcode.rule.stock.user,barcodes.model_barcode_rule,stock.group_stock_user,1,0,0,0 +access_barcode_rule_stock_manager,barcode.rule.stock.manager,barcodes.model_barcode_rule,stock.group_stock_manager,1,1,1,1 \ No newline at end of file diff --git a/addons/stock/static/src/js/widgets.js b/addons/stock/static/src/js/widgets.js index f9b1b48f799..6008fc6723d 100644 --- a/addons/stock/static/src/js/widgets.js +++ b/addons/stock/static/src/js/widgets.js @@ -174,12 +174,12 @@ function openerp_picking_widgets(instance){ 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); + self.getParent().scan_product_id(id,1,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); + self.getParent().scan_product_id(id,-1,op_id); }); this.$('.js_unfold').click(function(){ var op_id = $(this).parent().data('id'); @@ -362,7 +362,7 @@ function openerp_picking_widgets(instance){ 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){ + if(value>0) { disabled = false; } }); @@ -859,7 +859,7 @@ function openerp_picking_widgets(instance){ } }); }, - scan_product_id: function(product_id,increment,op_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, increment is the value to increment (-1 or 1) var self = this; return new instance.web.Model('stock.picking') .call('process_product_id_from_ui', [self.picking.id, product_id, op_id, increment]) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 434a2aecb01..3fb8a692ad3 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -23,12 +23,14 @@ from datetime import date, datetime from dateutil import relativedelta import json import time +import sets +import openerp from openerp.osv import fields, osv from openerp.tools.float_utils import float_compare, float_round from openerp.tools.translate import _ from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT -from openerp import SUPERUSER_ID, api +from openerp import SUPERUSER_ID, api, models import openerp.addons.decimal_precision as dp from openerp.addons.procurement import procurement import logging @@ -142,7 +144,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_id': fields.many2one('product.removal', 'Removal Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to take the products from, which lot etc. for this location. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."), 'putaway_strategy_id': fields.many2one('product.putaway', 'Put Away Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to store the products. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."), - 'loc_barcode': fields.char('Location Barcode'), + 'barcode': fields.char('Barcode', oldname='loc_barcode'), } _defaults = { 'active': True, @@ -153,11 +155,11 @@ 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 !')] + _sql_constraints = [('barcode_company_uniq', 'unique (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)}) + if not default.get('barcode', False): + default.update({'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): @@ -1471,44 +1473,63 @@ class stock_picking(osv.osv): 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, op_id, increment=True, context=None): + def process_product_id_from_ui(self, cr, uid, picking_id, product_id, op_id, increment=1, 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, 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: - 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 + + # Barcode Nomenclatures + picking_type_id = self.browse(cr, uid, [picking_id], context=context).picking_type_id.id + barcode_nom = self.pool.get('stock.picking.type').browse(cr, uid, [picking_type_id], context=context).barcode_nomenclature_id + parsed_result = barcode_nom.parse_barcode(barcode_str) + + #check if the barcode is a weighted barcode or simply a product + if parsed_result['type'] in ['weight', 'product', 'package']: + weight=1 + if parsed_result['type'] == 'weight': + domain = ['|', ('barcode', '=', parsed_result['base_code']), ('default_code', '=', parsed_result['base_code'])] + weight=parsed_result['value'] + obj = self.pool.get('product.product') + id_in_operation = 'product_id' + elif parsed_result['type'] == 'product': + domain = ['|', ('barcode', '=', parsed_result['code']), ('default_code', '=', parsed_result['code'])] + obj = self.pool.get('product.product') + id_in_operation = 'product_id' + else: + domain = [('name', '=', parsed_result['code'])] + obj = self.pool.get('stock.quant.package') + id_in_operation = 'package_id' + + matching_product_ids = obj.search(cr, uid, domain, context=context) + if matching_product_ids: + op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [(id_in_operation, '=', matching_product_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=weight, 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) - 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: - 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 + elif parsed_result['type'] == 'lot': + lot_obj = self.pool.get('stock.production.lot') + matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', parsed_result['code'])], context=context) + if matching_lot_ids: + lot = lot_obj.browse(cr, uid, matching_lot_ids[0], 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=1, context=context) + answer['operation_id'] = op_id + return answer + + #check if the barcode correspond to a location + elif parsed_result['type'] == 'location': + stock_location_obj = self.pool.get('stock.location') + matching_location_ids = stock_location_obj.search(cr, uid, [('barcode', '=', parsed_result['code'])], 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 + return answer @@ -3949,8 +3970,8 @@ class stock_pack_operation(osv.osv): new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, val, context=context) self.write(cr, uid, id, {'lot_id': new_lot_id}, context=context) - 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 + def _search_and_increment(self, cr, uid, picking_id, domain, filter_visible=False, visible_op_ids=False, increment=1, context=None): + '''Search for an operation with given 'domain' in a picking, if it exists increment the qty by the value of increment otherwise create it :param domain: list of tuple directly reusable as a domain context can receive a key 'current_package_id' with the package to consider for this operation @@ -3973,14 +3994,15 @@ class stock_pack_operation(osv.osv): 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 increment > 0: + qty += increment + elif increment < 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 + else: + qty = max(0, qty-1) 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 @@ -3991,7 +4013,7 @@ class stock_pack_operation(osv.osv): 'product_qty': 0, 'location_id': picking.location_id.id, 'location_dest_id': picking.location_dest_id.id, - 'qty_done': 1, + 'qty_done': increment, } for key in domain: var_name, dummy, value = key @@ -4300,10 +4322,32 @@ class stock_picking_type(osv.osv): 'rate_picking_backorders': fields.function(_get_picking_count, type='integer', multi='_get_picking_count'), + # Barcode nomenclature + 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcode Nomenclature', help='A barcode nomenclature', required=True), } + + def _get_default_nomenclature(self, cr, uid, context=None): + nom_obj = self.pool.get('barcode.nomenclature') + res = nom_obj.search(cr, uid, [], limit=1, context=context) + return res and res[0] or False + _defaults = { 'warehouse_id': _default_warehouse, 'active': True, + 'barcode_nomenclature_id': _get_default_nomenclature, } +class barcode_rule(models.Model): + _inherit = 'barcode.rule' + + def _get_type_selection(self): + types = sets.Set(super(barcode_rule,self)._get_type_selection()) + types.update([ + ('weight','Weighted Product'), + ('location','Location'), + ('lot','Lot'), + ('package','Package') + ]) + return list(types) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index 93e79767dfd..7072a26f6d8 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -40,8 +40,8 @@ parent="menu_product_in_config_stock" sequence="5" groups="product.group_uom"/> - + parent="menu_stock_configuration" sequence="35" groups="product.group_uom"/> + stock.inventory.line.tree stock.inventory.line @@ -349,7 +349,7 @@ - + @@ -1933,6 +1933,10 @@ - + + + + diff --git a/addons/stock/views/report_location_barcode.xml b/addons/stock/views/report_location_barcode.xml index 7e6466ed5c8..ecb9c4b0a96 100644 --- a/addons/stock/views/report_location_barcode.xml +++ b/addons/stock/views/report_location_barcode.xml @@ -10,11 +10,11 @@
- - + +

- - + +

diff --git a/addons/stock/views/report_stockpicking.xml b/addons/stock/views/report_stockpicking.xml index 7d2bb125de2..97131958ded 100644 --- a/addons/stock/views/report_stockpicking.xml +++ b/addons/stock/views/report_stockpicking.xml @@ -72,8 +72,8 @@ - - + + @@ -106,8 +106,8 @@ - - + + diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index c573e5cb53a..dab5a783e40 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -8,6 +8,7 @@ font-weight: normal; font-style: normal; } + @font-face { font-family: "EntypoRegular"; src: url("/web/static/src/font/entypo-webfont.eot") format("eot"); @@ -2025,6 +2026,10 @@ .openerp .oe_form .oe_form_field_image:hover .oe_form_field_image_controls { display: block; } +.openerp .oe_form .oe_form_field_image img { + max-width: 256px; + max-height: 256px; +} .openerp .oe_fileupload { display: inline-block; clear: both; @@ -2763,6 +2768,7 @@ top: 0px; } } + .kitten-mode-activated { background-size: cover; background-attachment: fixed; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index eeea472a5b0..fa37c1c1617 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1693,6 +1693,9 @@ $sheet-padding: 16px @include box-sizing(border) &:hover .oe_form_field_image_controls display: block + img + max-width: 256px + max-height: 256px .oe_fileupload display: inline-block clear: both diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 0d82ab7807d..41fbee1b78f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -250,7 +250,7 @@ class res_partner(osv.Model, format_address): 'comment': fields.text('Notes'), 'category_id': fields.many2many('res.partner.category', id1='partner_id', id2='category_id', string='Tags'), 'credit_limit': fields.float(string='Credit Limit'), - 'ean13': fields.char('EAN13', size=13), + 'barcode': fields.char('Barcode', oldname='ean13'), 'active': fields.boolean('Active'), 'customer': fields.boolean('Is a Customer', help="Check this box if this contact is a customer."), 'supplier': fields.boolean('Is a Supplier', help="Check this box if this contact is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."), @@ -395,24 +395,6 @@ class res_partner(osv.Model, format_address): return {'value': {'country_id': state.country_id.id}} return {} - def _check_ean_key(self, cr, uid, ids, context=None): - for partner_o in self.pool['res.partner'].read(cr, uid, ids, ['ean13',]): - thisean=partner_o['ean13'] - if thisean and thisean!='': - if len(thisean)!=13: - return False - sum=0 - for i in range(12): - if not (i % 2): - sum+=int(thisean[i]) - else: - sum+=3*int(thisean[i]) - if math.ceil(sum/10.0)*10-sum!=int(thisean[12]): - return False - return True - -# _constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean13'])] - def _update_fields_values(self, cr, uid, partner, fields, context=None): """ Returns dict of write() values for synchronizing ``fields`` """ values = {}