@@ -302,8 +305,8 @@
-
-
+
+
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index f75cda8cdde..c74ca5f833a 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -1045,6 +1045,8 @@ class account_voucher(osv.osv):
# if the amount encoded in voucher is equal to the amount unreconciled, we need to compute the
# currency rate difference
if line.amount == line.amount_unreconciled:
+ if not line.move_line_id.amount_residual:
+ raise osv.except_osv(_('Wrong bank statement line'),_("You have to delete the bank statement line which the payment was reconciled to manually. Please check the payment of the partner %s by the amount of %s.")%(line.voucher_id.partner_id.name, line.voucher_id.amount))
currency_rate_difference = line.move_line_id.amount_residual - amount
else:
currency_rate_difference = 0.0
diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml
index 64e1cf22156..ba9d6433888 100644
--- a/addons/account_voucher/account_voucher_view.xml
+++ b/addons/account_voucher/account_voucher_view.xml
@@ -75,7 +75,7 @@
-
+
diff --git a/addons/account_voucher/voucher_payment_receipt_view.xml b/addons/account_voucher/voucher_payment_receipt_view.xml
index 503c3aeaa5e..c41ed6d5577 100644
--- a/addons/account_voucher/voucher_payment_receipt_view.xml
+++ b/addons/account_voucher/voucher_payment_receipt_view.xml
@@ -102,9 +102,9 @@
-
+
-
+
@@ -194,7 +194,7 @@
-
+
@@ -231,8 +231,8 @@
-
-
+
+
@@ -366,7 +366,7 @@
-
+
@@ -401,7 +401,7 @@
-
+
diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py
index d90e2006a71..66a67ad4219 100644
--- a/addons/analytic/analytic.py
+++ b/addons/analytic/analytic.py
@@ -96,22 +96,6 @@ class account_analytic_account(osv.osv):
res[row['id']][field] = row[field]
return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context)
- def name_get(self, cr, uid, ids, context=None):
- if isinstance(ids, (int, long)):
- ids=[ids]
- if not ids:
- return []
- res = []
- for account in self.browse(cr, uid, ids, context=context):
- data = []
- acc = account
- while acc:
- data.insert(0, acc.name)
- acc = acc.parent_id
- data = ' / '.join(data)
- res.append((account.id, data))
- return res
-
def _complete_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict):
res = self.name_get(cr, uid, ids)
return dict(res)
diff --git a/addons/base_import/__init__.py b/addons/base_import/__init__.py
new file mode 100644
index 00000000000..939a8173e38
--- /dev/null
+++ b/addons/base_import/__init__.py
@@ -0,0 +1,3 @@
+import controllers
+import models
+import test_models
diff --git a/addons/base_import/__openerp__.py b/addons/base_import/__openerp__.py
new file mode 100644
index 00000000000..eff20bc90ab
--- /dev/null
+++ b/addons/base_import/__openerp__.py
@@ -0,0 +1,39 @@
+{
+ 'name': 'Base import',
+ 'description': """
+New extensible file import for OpenERP
+======================================
+
+Re-implement openerp's file import system:
+
+* Server side, the previous system forces most of the logic into the
+ client which duplicates the effort (between clients), makes the
+ import system much harder to use without a client (direct RPC or
+ other forms of automation) and makes knowledge about the
+ import/export system much harder to gather as it is spread over
+ 3+ different projects.
+
+* In a more extensible manner, so users and partners can build their
+ own front-end to import from other file formats (e.g. OpenDocument
+ files) which may be simpler to handle in their work flow or from
+ their data production sources.
+
+* In a module, so that administrators and users of OpenERP who do not
+ need or want an online import can avoid it being available to users.
+""",
+ 'category': 'Uncategorized',
+ 'website': 'http://www.openerp.com',
+ 'author': 'OpenERP SA',
+ 'depends': ['base'],
+ 'installable': True,
+ 'auto_install': False, # set to true and allow uninstall?
+ 'css': [
+ 'static/lib/select2/select2.css',
+ 'static/src/css/import.css',
+ ],
+ 'js': [
+ 'static/lib/select2/select2.js',
+ 'static/src/js/import.js',
+ ],
+ 'qweb': ['static/src/xml/import.xml'],
+}
diff --git a/addons/base_import/controllers.py b/addons/base_import/controllers.py
new file mode 100644
index 00000000000..85ff993e92e
--- /dev/null
+++ b/addons/base_import/controllers.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+import simplejson
+
+try:
+ import openerp.addons.web.common.http as openerpweb
+except ImportError:
+ import web.common.http as openerpweb
+
+class ImportController(openerpweb.Controller):
+ _cp_path = '/base_import'
+
+ @openerpweb.httprequest
+ def set_file(self, req, file, import_id, jsonp='callback'):
+ import_id = int(import_id)
+
+ written = req.session.model('base_import.import').write(import_id, {
+ 'file': file.read(),
+ 'file_name': file.filename,
+ 'file_type': file.content_type,
+ }, req.session.eval_context(req.context))
+
+ return 'window.top.%s(%s)' % (
+ jsonp, simplejson.dumps({'result': written}))
diff --git a/addons/base_import/models.py b/addons/base_import/models.py
new file mode 100644
index 00000000000..1e5ad002b84
--- /dev/null
+++ b/addons/base_import/models.py
@@ -0,0 +1,352 @@
+import csv
+import itertools
+import logging
+import operator
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+import psycopg2
+
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+
+FIELDS_RECURSION_LIMIT = 2
+ERROR_PREVIEW_BYTES = 200
+_logger = logging.getLogger(__name__)
+class ir_import(orm.TransientModel):
+ _name = 'base_import.import'
+ # allow imports to survive for 12h in case user is slow
+ _transient_max_hours = 12.0
+
+ _columns = {
+ 'res_model': fields.char('Model', size=64),
+ 'file': fields.binary(
+ 'File', help="File to check and/or import, raw binary (not base64)"),
+ 'file_name': fields.char('File Name', size=None),
+ 'file_type': fields.char('File Type', size=None),
+ }
+
+ def get_fields(self, cr, uid, model, context=None,
+ depth=FIELDS_RECURSION_LIMIT):
+ """ Recursively get fields for the provided model (through
+ fields_get) and filter them according to importability
+
+ The output format is a list of ``Field``, with ``Field``
+ defined as:
+
+ .. class:: Field
+
+ .. attribute:: id (str)
+
+ A non-unique identifier for the field, used to compute
+ the span of the ``required`` attribute: if multiple
+ ``required`` fields have the same id, only one of them
+ is necessary.
+
+ .. attribute:: name (str)
+
+ The field's logical (OpenERP) name within the scope of
+ its parent.
+
+ .. attribute:: string (str)
+
+ The field's human-readable name (``@string``)
+
+ .. attribute:: required (bool)
+
+ Whether the field is marked as required in the
+ model. Clients must provide non-empty import values
+ for all required fields or the import will error out.
+
+ .. attribute:: fields (list(Field))
+
+ The current field's subfields. The database and
+ external identifiers for m2o and m2m fields; a
+ filtered and transformed fields_get for o2m fields (to
+ a variable depth defined by ``depth``).
+
+ Fields with no sub-fields will have an empty list of
+ sub-fields.
+
+ :param str model: name of the model to get fields form
+ :param int landing: depth of recursion into o2m fields
+ """
+ fields = [{
+ 'id': 'id',
+ 'name': 'id',
+ 'string': _("External ID"),
+ 'required': False,
+ 'fields': [],
+ }]
+ fields_got = self.pool[model].fields_get(cr, uid, context=context)
+ for name, field in fields_got.iteritems():
+ if field.get('readonly'):
+ states = field.get('states')
+ if not states:
+ continue
+ # states = {state: [(attr, value), (attr2, value2)], state2:...}
+ if not any(attr == 'readonly' and value is False
+ for attr, value in itertools.chain.from_iterable(
+ states.itervalues())):
+ continue
+
+ f = {
+ 'id': name,
+ 'name': name,
+ 'string': field['string'],
+ # Y U NO ALWAYS HAVE REQUIRED
+ 'required': bool(field.get('required')),
+ 'fields': [],
+ }
+
+ if field['type'] in ('many2many', 'many2one'):
+ f['fields'] = [
+ dict(f, name='id', string=_("External ID")),
+ dict(f, name='.id', string=_("Database ID")),
+ ]
+ elif field['type'] == 'one2many' and depth:
+ f['fields'] = self.get_fields(
+ cr, uid, field['relation'], context=context, depth=depth-1)
+
+ fields.append(f)
+
+ # TODO: cache on model?
+ return fields
+
+ def _read_csv(self, record, options):
+ """ Returns a CSV-parsed iterator of all empty lines in the file
+
+ :throws csv.Error: if an error is detected during CSV parsing
+ :throws UnicodeDecodeError: if ``options.encoding`` is incorrect
+ """
+ csv_iterator = csv.reader(
+ StringIO(record.file),
+ quotechar=options['quoting'],
+ delimiter=options['separator'])
+ csv_nonempty = itertools.ifilter(None, csv_iterator)
+ # TODO: guess encoding with chardet? Or https://github.com/aadsm/jschardet
+ encoding = options.get('encoding', 'utf-8')
+ return itertools.imap(
+ lambda row: [item.decode(encoding) for item in row],
+ csv_nonempty)
+
+ def _match_header(self, header, fields, options):
+ """ Attempts to match a given header to a field of the
+ imported model.
+
+ :param str header: header name from the CSV file
+ :param fields:
+ :param dict options:
+ :returns: an empty list if the header couldn't be matched, or
+ all the fields to traverse
+ :rtype: list(Field)
+ """
+ for field in fields:
+ # FIXME: should match all translations & original
+ # TODO: use string distance (levenshtein? hamming?)
+ if header == field['name'] \
+ or header.lower() == field['string'].lower():
+ return [field]
+
+ if '/' not in header:
+ return []
+
+ # relational field path
+ traversal = []
+ subfields = fields
+ # Iteratively dive into fields tree
+ for section in header.split('/'):
+ # Strip section in case spaces are added around '/' for
+ # readability of paths
+ match = self._match_header(section.strip(), subfields, options)
+ # Any match failure, exit
+ if not match: return []
+ # prep subfields for next iteration within match[0]
+ field = match[0]
+ subfields = field['fields']
+ traversal.append(field)
+ return traversal
+
+ def _match_headers(self, rows, fields, options):
+ """ Attempts to match the imported model's fields to the
+ titles of the parsed CSV file, if the file is supposed to have
+ headers.
+
+ Will consume the first line of the ``rows`` iterator.
+
+ Returns a pair of (None, None) if headers were not requested
+ or the list of headers and a dict mapping cell indices
+ to key paths in the ``fields`` tree
+
+ :param Iterator rows:
+ :param dict fields:
+ :param dict options:
+ :rtype: (None, None) | (list(str), dict(int: list(str)))
+ """
+ if not options.get('headers'):
+ return None, None
+
+ headers = next(rows)
+ return headers, dict(
+ (index, [field['name'] for field in self._match_header(header, fields, options)] or None)
+ for index, header in enumerate(headers)
+ )
+
+ def parse_preview(self, cr, uid, id, options, count=10, context=None):
+ """ Generates a preview of the uploaded files, and performs
+ fields-matching between the import's file data and the model's
+ columns.
+
+ If the headers are not requested (not options.headers),
+ ``matches`` and ``headers`` are both ``False``.
+
+ :param id: identifier of the import
+ :param int count: number of preview lines to generate
+ :param options: format-specific options.
+ CSV: {encoding, quoting, separator, headers}
+ :type options: {str, str, str, bool}
+ :returns: {fields, matches, headers, preview} | {error, preview}
+ :rtype: {dict(str: dict(...)), dict(int, list(str)), list(str), list(list(str))} | {str, str}
+ """
+ (record,) = self.browse(cr, uid, [id], context=context)
+ fields = self.get_fields(cr, uid, record.res_model, context=context)
+
+ try:
+ rows = self._read_csv(record, options)
+
+ headers, matches = self._match_headers(rows, fields, options)
+ # Match should have consumed the first row (iif headers), get
+ # the ``count`` next rows for preview
+ preview = itertools.islice(rows, count)
+ return {
+ 'fields': fields,
+ 'matches': matches or False,
+ 'headers': headers or False,
+ 'preview': list(preview),
+ }
+ except Exception, e:
+ # Due to lazy generators, UnicodeDecodeError (for
+ # instance) may only be raised when serializing the
+ # preview to a list in the return.
+ _logger.debug("Error during CSV parsing preview", exc_info=True)
+ return {
+ 'error': str(e),
+ # iso-8859-1 ensures decoding will always succeed,
+ # even if it yields non-printable characters. This is
+ # in case of UnicodeDecodeError (or csv.Error
+ # compounded with UnicodeDecodeError)
+ 'preview': record.file[:ERROR_PREVIEW_BYTES]
+ .decode( 'iso-8859-1'),
+ }
+
+ def _convert_import_data(self, record, fields, options, context=None):
+ """ Extracts the input browse_record and fields list (with
+ ``False``-y placeholders for fields to *not* import) into a
+ format Model.import_data can use: a fields list without holes
+ and the precisely matching data matrix
+
+ :param browse_record record:
+ :param list(str|bool): fields
+ :returns: (data, fields)
+ :rtype: (list(list(str)), list(str))
+ :raises ValueError: in case the import data could not be converted
+ """
+ # Get indices for non-empty fields
+ indices = [index for index, field in enumerate(fields) if field]
+ if not indices:
+ raise ValueError(_("You must configure at least one field to import"))
+ # If only one index, itemgetter will return an atom rather
+ # than a 1-tuple
+ if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
+ else: mapper = operator.itemgetter(*indices)
+ # Get only list of actually imported fields
+ import_fields = filter(None, fields)
+
+ rows_to_import = self._read_csv(record, options)
+ if options.get('headers'):
+ rows_to_import = itertools.islice(
+ rows_to_import, 1, None)
+ data = [
+ row for row in itertools.imap(mapper, rows_to_import)
+ # don't try inserting completely empty rows (e.g. from
+ # filtering out o2m fields)
+ if any(row)
+ ]
+
+ return data, import_fields
+
+ def do(self, cr, uid, id, fields, options, dryrun=False, context=None):
+ """ Actual execution of the import
+
+ :param fields: import mapping: maps each column to a field,
+ ``False`` for the columns to ignore
+ :type fields: list(str|bool)
+ :param dict options:
+ :param bool dryrun: performs all import operations (and
+ validations) but rollbacks writes, allows
+ getting as much errors as possible without
+ the risk of clobbering the database.
+ :returns: A list of errors. If the list is empty the import
+ executed fully and correctly. If the list is
+ non-empty it contains dicts with 3 keys ``type`` the
+ type of error (``error|warning``); ``message`` the
+ error message associated with the error (a string)
+ and ``record`` the data which failed to import (or
+ ``false`` if that data isn't available or provided)
+ :rtype: list({type, message, record})
+ """
+ cr.execute('SAVEPOINT import')
+
+ (record,) = self.browse(cr, uid, [id], context=context)
+ try:
+ data, import_fields = self._convert_import_data(
+ record, fields, options, context=context)
+ except ValueError, e:
+ return [{
+ 'type': 'error',
+ 'message': str(e),
+ 'record': False,
+ }]
+
+ try:
+ _logger.info('importing %d rows...', len(data))
+ (code, record, message, _wat) = self.pool[record.res_model].import_data(
+ cr, uid, import_fields, data, context=context)
+ _logger.info('done')
+
+ except Exception, e:
+ _logger.exception("Import failed")
+ # TODO: remove when exceptions stop being an "expected"
+ # behavior of import_data on some (most) invalid
+ # input.
+ code, record, message = -1, None, str(e)
+
+ # If transaction aborted, RELEASE SAVEPOINT is going to raise
+ # an InternalError (ROLLBACK should work, maybe). Ignore that.
+ # TODO: to handle multiple errors, create savepoint around
+ # write and release it in case of write error (after
+ # adding error to errors array) => can keep on trying to
+ # import stuff, and rollback at the end if there is any
+ # error in the results.
+ try:
+ if dryrun:
+ cr.execute('ROLLBACK TO SAVEPOINT import')
+ else:
+ cr.execute('RELEASE SAVEPOINT import')
+ except psycopg2.InternalError:
+ pass
+
+ if code != -1:
+ return []
+
+ # TODO: add key for error location?
+ # TODO: error not within normal preview, how to display? Re-preview
+ # with higher ``count``?
+ return [{
+ 'type': 'error',
+ 'message': message,
+ 'record': record or False
+ }]
diff --git a/addons/base_import/static/lib/select2/LICENSE b/addons/base_import/static/lib/select2/LICENSE
new file mode 100644
index 00000000000..627fddef413
--- /dev/null
+++ b/addons/base_import/static/lib/select2/LICENSE
@@ -0,0 +1,12 @@
+Copyright 2012 Igor Vaynberg
+
+Version: @@ver@@ Timestamp: @@timestamp@@
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is
+distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and limitations under the License.
\ No newline at end of file
diff --git a/addons/base_import/static/lib/select2/README.md b/addons/base_import/static/lib/select2/README.md
new file mode 100755
index 00000000000..3a6dcaf1ba4
--- /dev/null
+++ b/addons/base_import/static/lib/select2/README.md
@@ -0,0 +1,68 @@
+Select2
+=================
+
+Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results. Look and feel of Select2 is based on the excellent [Chosen](http://harvesthq.github.com/chosen/) library.
+
+To get started -- checkout http://ivaynberg.github.com/select2!
+
+What Does Select2 Support That Chosen Does Not?
+-------------------------------------------------
+
+* Working with large datasets: Chosen requires the entire dataset to be loaded as `option` tags in the DOM, which limits
+it to working with small-ish datasets. Select2 uses a function to find results on-the-fly, which allows it to partially
+load results.
+* Paging of results: Since Select2 works with large datasets and only loads a small amount of matching results at a time
+it has to support paging. Select2 will call the search function when the user scrolls to the bottom of currently loaded
+result set allowing for the 'infinite scrolling' of results.
+* Custom markup for results: Chosen only supports rendering text results because that is the only markup supported by
+`option` tags. Select2 provides an extension point which can be used to produce any kind of markup to represent results.
+* Ability to add results on the fly: Select2 provides the ability to add results from the search term entered by the user, which allows it to be used for
+tagging.
+
+Browser Compatibility
+--------------------
+* IE 8+ (7 mostly works except for [issue with z-index](https://github.com/ivaynberg/select2/issues/37))
+* Chrome 8+
+* Firefox 3.5+
+* Safari 3+
+* Opera 10.6+
+
+Integrations
+------------
+
+* [Wicket-Select2](https://github.com/ivaynberg/wicket-select2) (Java / Apache Wicket)
+* [select2-rails](https://github.com/argerim/select2-rails) (Ruby on Rails)
+* [AngularUI](http://angular-ui.github.com/#directives-select2) ([AngularJS](angularjs.org))
+* [Django](https://github.com/applegrew/django-select2)
+
+Bug tracker
+-----------
+
+Have a bug? Please create an issue here on GitHub!
+
+https://github.com/ivaynberg/select2/issues
+
+
+Mailing list
+------------
+
+Have a question? Ask on our mailing list!
+
+select2@googlegroups.com
+
+https://groups.google.com/d/forum/select2
+
+
+Copyright and License
+---------------------
+
+Copyright 2012 Igor Vaynberg
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is
+distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and limitations under the License.
\ No newline at end of file
diff --git a/addons/base_import/static/lib/select2/release.sh b/addons/base_import/static/lib/select2/release.sh
new file mode 100755
index 00000000000..79eaabd621d
--- /dev/null
+++ b/addons/base_import/static/lib/select2/release.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+set -e
+
+echo -n "Enter the version for this release: "
+
+read ver
+
+if [ ! $ver ]; then
+ echo "Invalid version."
+ exit
+fi
+
+name="select2"
+js="$name.js"
+mini="$name.min.js"
+css="$name.css"
+release="$name-$ver"
+releasedir="/tmp/$release"
+tag="release-$ver"
+branch="build-$ver"
+curbranch=`git branch | grep "*" | sed "s/* //"`
+timestamp=$(date)
+tokens="s/@@ver@@/$ver/g;s/\@@timestamp@@/$timestamp/g"
+remote="github"
+
+git branch "$branch"
+git checkout "$branch"
+
+echo "Tokenizing..."
+
+find . -name "$js" | xargs sed -i -e "$tokens"
+find . -name "$css" | xargs sed -i -e "$tokens"
+
+git add "$js"
+git add "$css"
+
+echo "Minifying..."
+
+echo "/*" > "$mini"
+cat LICENSE | sed "$tokens" >> "$mini"
+echo "*/" >> "$mini"
+
+curl -s \
+ -d compilation_level=SIMPLE_OPTIMIZATIONS \
+ -d output_format=text \
+ -d output_info=compiled_code \
+ --data-urlencode "js_code@$js" \
+ http://closure-compiler.appspot.com/compile \
+ >> "$mini"
+
+git add "$mini"
+
+git commit -m "release $ver"
+
+echo "Tagging..."
+
+git tag -a "$tag" -m "tagged version $ver"
+git push "$remote" --tags
+
+echo "Archiving..."
+
+rm -rf "$releasedir"
+mkdir "$releasedir"
+
+cp $name.* "$releasedir"
+cp spinner.gif "$releasedir"
+cp README.* "$releasedir"
+
+zip -r "$releasedir.zip" "$releasedir"
+rm -rf "$releasedir"
+
+echo "Cleaning Up..."
+
+git checkout "$curbranch"
+git branch -D "$branch"
+
+echo "Done. Release archive created: $releasedir.zip"
diff --git a/addons/base_import/static/lib/select2/select2.css b/addons/base_import/static/lib/select2/select2.css
new file mode 100755
index 00000000000..f0bdfb82f04
--- /dev/null
+++ b/addons/base_import/static/lib/select2/select2.css
@@ -0,0 +1,524 @@
+/*
+Version: @@ver@@ Timestamp: @@timestamp@@
+*/
+.select2-container {
+ position: relative;
+ display: inline-block;
+ /* inline-block for ie7 */
+ zoom: 1;
+ *display: inline;
+ vertical-align: top;
+}
+
+.select2-container,
+.select2-drop,
+.select2-search,
+.select2-search input{
+ /*
+ Force border-box so that % widths fit the parent
+ container without overlap because of margin/padding.
+
+ More Info : http://www.quirksmode.org/css/box.html
+ */
+ -moz-box-sizing: border-box; /* firefox */
+ -ms-box-sizing: border-box; /* ie */
+ -webkit-box-sizing: border-box; /* webkit */
+ -khtml-box-sizing: border-box; /* konqueror */
+ box-sizing: border-box; /* css3 */
+}
+
+.select2-container .select2-choice {
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white));
+ background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%);
+ background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%);
+ background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%);
+ background-image: -ms-linear-gradient(top, #eeeeee 0%, #ffffff 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#ffffff', GradientType = 0);
+ background-image: linear-gradient(top, #eeeeee 0%, #ffffff 50%);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -moz-background-clip: padding;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box;
+ border: 1px solid #aaa;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ position: relative;
+ height: 26px;
+ line-height: 26px;
+ padding: 0 0 0 8px;
+ color: #444;
+ text-decoration: none;
+}
+
+.select2-container.select2-drop-above .select2-choice
+{
+ border-bottom-color: #aaa;
+ -webkit-border-radius:0px 0px 4px 4px;
+ -moz-border-radius:0px 0px 4px 4px;
+ border-radius:0px 0px 4px 4px;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white));
+ background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%);
+ background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%);
+ background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%);
+ background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
+ background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%);
+}
+
+.select2-container .select2-choice span {
+ margin-right: 26px;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ -o-text-overflow: ellipsis;
+ -ms-text-overflow: ellipsis;
+ text-overflow: ellipsis;
+}
+
+.select2-container .select2-choice abbr {
+ display: block;
+ position: absolute;
+ right: 26px;
+ top: 8px;
+ width: 12px;
+ height: 12px;
+ font-size: 1px;
+ background: url('select2.png') right top no-repeat;
+ cursor: pointer;
+ text-decoration: none;
+ border:0;
+ outline: 0;
+}
+.select2-container .select2-choice abbr:hover {
+ background-position: right -11px;
+ cursor: pointer;
+}
+
+.select2-drop {
+ background: #fff;
+ color: #000;
+ border: 1px solid #aaa;
+ border-top: 0;
+ position: absolute;
+ top: 100%;
+ -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ -o-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ z-index: 9999;
+ width:100%;
+ margin-top:-1px;
+
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius: 0 0 4px 4px;
+ border-radius: 0 0 4px 4px;
+}
+
+.select2-drop.select2-drop-above {
+ -webkit-border-radius: 4px 4px 0px 0px;
+ -moz-border-radius: 4px 4px 0px 0px;
+ border-radius: 4px 4px 0px 0px;
+ margin-top:1px;
+ border-top: 1px solid #aaa;
+ border-bottom: 0;
+
+ -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ -moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ -o-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+}
+
+.select2-container .select2-choice div {
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+ -moz-background-clip: padding;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box;
+ background: #ccc;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
+ background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+ background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+ background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%);
+ background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#cccccc', endColorstr = '#eeeeee', GradientType = 0);
+ background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%);
+ border-left: 1px solid #aaa;
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: block;
+ height: 100%;
+ width: 18px;
+}
+
+.select2-container .select2-choice div b {
+ background: url('select2.png') no-repeat 0 1px;
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.select2-search {
+ display: inline-block;
+ white-space: nowrap;
+ z-index: 10000;
+ min-height: 26px;
+ width: 100%;
+ margin: 0;
+ padding-left: 4px;
+ padding-right: 4px;
+}
+
+.select2-search-hidden {
+ display: block;
+ position: absolute;
+ left: -10000px;
+}
+
+.select2-search input {
+ background: #fff url('select2.png') no-repeat 100% -22px;
+ background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+ background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+ background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+ padding: 4px 20px 4px 5px;
+ outline: 0;
+ border: 1px solid #aaa;
+ font-family: sans-serif;
+ font-size: 1em;
+ width:100%;
+ margin:0;
+ height:auto !important;
+ min-height: 26px;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ border-radius: 0;
+ -moz-border-radius: 0;
+ -webkit-border-radius: 0;
+}
+
+.select2-drop.select2-drop-above .select2-search input
+{
+ margin-top:4px;
+}
+
+.select2-search input.select2-active {
+ background: #fff url('spinner.gif') no-repeat 100%;
+ background: url('spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+ background: url('spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+ background: url('spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+}
+
+
+.select2-container-active .select2-choice,
+.select2-container-active .select2-choices {
+ -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+ -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ -o-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ box-shadow : 0 0 5px rgba(0,0,0,.3);
+ border: 1px solid #5897fb;
+ outline: none;
+}
+
+.select2-dropdown-open .select2-choice {
+ border: 1px solid #aaa;
+ border-bottom-color: transparent;
+ -webkit-box-shadow: 0 1px 0 #fff inset;
+ -moz-box-shadow : 0 1px 0 #fff inset;
+ -o-box-shadow : 0 1px 0 #fff inset;
+ box-shadow : 0 1px 0 #fff inset;
+ background-color: #eee;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee));
+ background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%);
+ background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%);
+ background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%);
+ background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
+ background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%);
+ -webkit-border-bottom-left-radius : 0;
+ -webkit-border-bottom-right-radius: 0;
+ -moz-border-radius-bottomleft : 0;
+ -moz-border-radius-bottomright: 0;
+ border-bottom-left-radius : 0;
+ border-bottom-right-radius: 0;
+}
+
+.select2-dropdown-open .select2-choice div {
+ background: transparent;
+ border-left: none;
+}
+.select2-dropdown-open .select2-choice div b {
+ background-position: -18px 1px;
+}
+
+/* results */
+.select2-results {
+ margin: 4px 4px 4px 0;
+ padding: 0 0 0 4px;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+ max-height: 200px;
+}
+
+.select2-results ul.select2-result-sub {
+ margin: 0 0 0 0;
+}
+
+.select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px }
+.select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px }
+
+.select2-results li {
+ list-style: none;
+ display: list-item;
+}
+
+.select2-results li.select2-result-with-children > .select2-result-label {
+ font-weight: bold;
+}
+
+.select2-results .select2-result-label {
+ padding: 3px 7px 4px;
+ margin: 0;
+ cursor: pointer;
+}
+
+.select2-results .select2-highlighted {
+ background: #3875d7;
+ color: #fff;
+}
+.select2-results li em {
+ background: #feffde;
+ font-style: normal;
+}
+.select2-results .select2-highlighted em {
+ background: transparent;
+}
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-selection-limit {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/*
+disabled look for already selected choices in the results dropdown
+.select2-results .select2-disabled.select2-highlighted {
+ color: #666;
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+.select2-results .select2-disabled {
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+*/
+.select2-results .select2-disabled {
+ display: none;
+}
+
+.select2-more-results.select2-active {
+ background: #f4f4f4 url('spinner.gif') no-repeat 100%;
+}
+
+.select2-more-results {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/* disabled styles */
+
+.select2-container.select2-container-disabled .select2-choice {
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container.select2-container-disabled .select2-choice div {
+ background-color: #f4f4f4;
+ background-image: none;
+ border-left: 0;
+}
+
+
+/* multiselect */
+
+.select2-container-multi .select2-choices {
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+ background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+ border: 1px solid #aaa;
+ margin: 0;
+ padding: 0;
+ cursor: text;
+ overflow: hidden;
+ height: auto !important;
+ height: 1%;
+ position: relative;
+}
+
+.select2-container-multi .select2-choices {
+ min-height: 26px;
+}
+
+.select2-container-multi.select2-container-active .select2-choices {
+ -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+ -moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ -o-box-shadow : 0 0 5px rgba(0,0,0,.3);
+ box-shadow : 0 0 5px rgba(0,0,0,.3);
+ border: 1px solid #5897fb;
+ outline: none;
+}
+.select2-container-multi .select2-choices li {
+ float: left;
+ list-style: none;
+}
+.select2-container-multi .select2-choices .select2-search-field {
+ white-space: nowrap;
+ margin: 0;
+ padding: 0;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input {
+ color: #666;
+ background: transparent !important;
+ font-family: sans-serif;
+ font-size: 100%;
+ height: 15px;
+ padding: 5px;
+ margin: 1px 0;
+ outline: 0;
+ border: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow : none;
+ -o-box-shadow : none;
+ box-shadow : none;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input.select2-active {
+ background: #fff url('spinner.gif') no-repeat 100% !important;
+}
+
+.select2-default {
+ color: #999 !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
+ -webkit-border-radius: 3px;
+ -moz-border-radius : 3px;
+ border-radius : 3px;
+ -moz-background-clip : padding;
+ -webkit-background-clip: padding-box;
+ background-clip : padding-box;
+ background-color: #e4e4e4;
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 );
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+ -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ -moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+ color: #333;
+ border: 1px solid #aaaaaa;
+ line-height: 13px;
+ padding: 3px 5px 3px 18px;
+ margin: 3px 0 3px 5px;
+ position: relative;
+ cursor: default;
+}
+.select2-container-multi .select2-choices .select2-search-choice span {
+ cursor: default;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus {
+ background: #d4d4d4;
+}
+
+.select2-search-choice-close {
+ display: block;
+ position: absolute;
+ right: 3px;
+ top: 4px;
+ width: 12px;
+ height: 13px;
+ font-size: 1px;
+ background: url('select2.png') right top no-repeat;
+ outline: none;
+}
+
+.select2-container-multi .select2-search-choice-close {
+ left: 3px;
+}
+
+
+.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
+ background-position: right -11px;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
+ background-position: right -11px;
+}
+
+/* disabled styles */
+
+.select2-container-multi.select2-container-disabled .select2-choices{
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
+ background-image: none;
+ background-color: #f4f4f4;
+ border: 1px solid #ddd;
+ padding: 3px 5px 3px 5px;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {
+ display: none;
+}
+/* end multiselect */
+
+.select2-result-selectable .select2-match,
+.select2-result-unselectable .select2-result-selectable .select2-match { text-decoration: underline; }
+.select2-result-unselectable .select2-match { text-decoration: none; }
+
+.select2-offscreen { position: absolute; left: -10000px; }
+
+/* Retina-ize icons */
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5) {
+ .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice div b {
+ background-image: url(select2x2.png) !important;
+ background-repeat: no-repeat !important;
+ background-size: 60px 40px !important;
+ }
+ .select2-search input {
+ background-position: 100% -21px !important;
+ }
+}
\ No newline at end of file
diff --git a/addons/base_import/static/lib/select2/select2.js b/addons/base_import/static/lib/select2/select2.js
new file mode 100755
index 00000000000..42ce0a928b8
--- /dev/null
+++ b/addons/base_import/static/lib/select2/select2.js
@@ -0,0 +1,2407 @@
+/*
+ Copyright 2012 Igor Vaynberg
+
+ Version: @@ver@@ Timestamp: @@timestamp@@
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+ compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the License is
+ distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and limitations under the License.
+ */
+ (function ($) {
+ if(typeof $.fn.each2 == "undefined"){
+ $.fn.extend({
+ /*
+ * 4-10 times faster .each replacement
+ * use it carefully, as it overrides jQuery context of element on each iteration
+ */
+ each2 : function (c) {
+ var j = $([0]), i = -1, l = this.length;
+ while (
+ ++i < l
+ && (j.context = j[0] = this[i])
+ && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
+ );
+ return this;
+ }
+ });
+ }
+})(jQuery);
+
+(function ($, undefined) {
+ "use strict";
+ /*global document, window, jQuery, console */
+
+ if (window.Select2 !== undefined) {
+ return;
+ }
+
+ var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer;
+
+ KEY = {
+ TAB: 9,
+ ENTER: 13,
+ ESC: 27,
+ SPACE: 32,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ HOME: 36,
+ END: 35,
+ BACKSPACE: 8,
+ DELETE: 46,
+ isArrow: function (k) {
+ k = k.which ? k.which : k;
+ switch (k) {
+ case KEY.LEFT:
+ case KEY.RIGHT:
+ case KEY.UP:
+ case KEY.DOWN:
+ return true;
+ }
+ return false;
+ },
+ isControl: function (e) {
+ var k = e.which;
+ switch (k) {
+ case KEY.SHIFT:
+ case KEY.CTRL:
+ case KEY.ALT:
+ return true;
+ }
+
+ if (e.metaKey) return true;
+
+ return false;
+ },
+ isFunctionKey: function (k) {
+ k = k.which ? k.which : k;
+ return k >= 112 && k <= 123;
+ }
+ };
+
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
+
+ function indexOf(value, array) {
+ var i = 0, l = array.length, v;
+
+ if (typeof value === "undefined") {
+ return -1;
+ }
+
+ if (value.constructor === String) {
+ for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i;
+ } else {
+ for (; i < l; i = i + 1) {
+ v = array[i];
+ if (v.constructor === String) {
+ if (v.localeCompare(value) === 0) return i;
+ } else {
+ if (v === value) return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Compares equality of a and b taking into account that a and b may be strings, in which case localeCompare is used
+ * @param a
+ * @param b
+ */
+ function equal(a, b) {
+ if (a === b) return true;
+ if (a === undefined || b === undefined) return false;
+ if (a === null || b === null) return false;
+ if (a.constructor === String) return a.localeCompare(b) === 0;
+ if (b.constructor === String) return b.localeCompare(a) === 0;
+ return false;
+ }
+
+ /**
+ * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
+ * strings
+ * @param string
+ * @param separator
+ */
+ function splitVal(string, separator) {
+ var val, i, l;
+ if (string === null || string.length < 1) return [];
+ val = string.split(separator);
+ for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
+ return val;
+ }
+
+ function getSideBorderPadding(element) {
+ return element.outerWidth() - element.width();
+ }
+
+ function installKeyUpChangeEvent(element) {
+ var key="keyup-change-value";
+ element.bind("keydown", function () {
+ if ($.data(element, key) === undefined) {
+ $.data(element, key, element.val());
+ }
+ });
+ element.bind("keyup", function () {
+ var val= $.data(element, key);
+ if (val !== undefined && element.val() !== val) {
+ $.removeData(element, key);
+ element.trigger("keyup-change");
+ }
+ });
+ }
+
+ $(document).delegate("*", "mousemove", function (e) {
+ $.data(document, "select2-lastpos", {x: e.pageX, y: e.pageY});
+ });
+
+ /**
+ * filters mouse events so an event is fired only if the mouse moved.
+ *
+ * filters out mouse events that occur when mouse is stationary but
+ * the elements under the pointer are scrolled.
+ */
+ function installFilteredMouseMove(element) {
+ element.bind("mousemove", function (e) {
+ var lastpos = $.data(document, "select2-lastpos");
+ if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
+ $(e.target).trigger("mousemove-filtered", e);
+ }
+ });
+ }
+
+ /**
+ * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
+ * within the last quietMillis milliseconds.
+ *
+ * @param quietMillis number of milliseconds to wait before invoking fn
+ * @param fn function to be debounced
+ * @param ctx object to be used as this reference within fn
+ * @return debounced version of fn
+ */
+ function debounce(quietMillis, fn, ctx) {
+ ctx = ctx || undefined;
+ var timeout;
+ return function () {
+ var args = arguments;
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function() {
+ fn.apply(ctx, args);
+ }, quietMillis);
+ };
+ }
+
+ /**
+ * A simple implementation of a thunk
+ * @param formula function used to lazily initialize the thunk
+ * @return {Function}
+ */
+ function thunk(formula) {
+ var evaluated = false,
+ value;
+ return function() {
+ if (evaluated === false) { value = formula(); evaluated = true; }
+ return value;
+ };
+ };
+
+ function installDebouncedScroll(threshold, element) {
+ var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
+ element.bind("scroll", function (e) {
+ if (indexOf(e.target, element.get()) >= 0) notify(e);
+ });
+ }
+
+ function killEvent(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ function measureTextWidth(e) {
+ if (!sizer){
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
+ sizer = $("").css({
+ position: "absolute",
+ left: "-10000px",
+ top: "-10000px",
+ display: "none",
+ fontSize: style.fontSize,
+ fontFamily: style.fontFamily,
+ fontStyle: style.fontStyle,
+ fontWeight: style.fontWeight,
+ letterSpacing: style.letterSpacing,
+ textTransform: style.textTransform,
+ whiteSpace: "nowrap"
+ });
+ $("body").append(sizer);
+ }
+ sizer.text(e.val());
+ return sizer.width();
+ }
+
+ function markMatch(text, term, markup) {
+ var match=text.toUpperCase().indexOf(term.toUpperCase()),
+ tl=term.length;
+
+ if (match<0) {
+ markup.push(text);
+ return;
+ }
+
+ markup.push(text.substring(0, match));
+ markup.push("");
+ markup.push(text.substring(match, match + tl));
+ markup.push("");
+ markup.push(text.substring(match + tl, text.length));
+ }
+
+ /**
+ * Produces an ajax-based query function
+ *
+ * @param options object containing configuration paramters
+ * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
+ * @param options.url url for the data
+ * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
+ * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
+ * @param options.traditional a boolean flag that should be true if you wish to use the traditional style of param serialization for the ajax request
+ * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
+ * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
+ * The expected format is an object containing the following keys:
+ * results array of objects that will be used as choices
+ * more (optional) boolean indicating whether there are more results available
+ * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
+ */
+ function ajax(options) {
+ var timeout, // current scheduled but not yet executed request
+ requestSequence = 0, // sequence used to drop out-of-order responses
+ handler = null,
+ quietMillis = options.quietMillis || 100;
+
+ return function (query) {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function () {
+ requestSequence += 1; // increment the sequence
+ var requestNumber = requestSequence, // this request's sequence number
+ data = options.data, // ajax data function
+ transport = options.transport || $.ajax,
+ traditional = options.traditional || false,
+ type = options.type || 'GET'; // set type of request (GET or POST)
+
+ data = data.call(this, query.term, query.page, query.context);
+
+ if( null !== handler) { handler.abort(); }
+
+ handler = transport.call(null, {
+ url: options.url,
+ dataType: options.dataType,
+ data: data,
+ type: type,
+ traditional: traditional,
+ success: function (data) {
+ if (requestNumber < requestSequence) {
+ return;
+ }
+ // TODO 3.0 - replace query.page with query so users have access to term, page, etc.
+ var results = options.results(data, query.page);
+ query.callback(results);
+ }
+ });
+ }, quietMillis);
+ };
+ }
+
+ /**
+ * Produces a query function that works with a local array
+ *
+ * @param options object containing configuration parameters. The options parameter can either be an array or an
+ * object.
+ *
+ * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
+ *
+ * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
+ * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
+ * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
+ * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
+ * the text.
+ */
+ function local(options) {
+ var data = options, // data elements
+ dataText,
+ text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
+
+ if (!$.isArray(data)) {
+ text = data.text;
+ // if text is not a function we assume it to be a key name
+ if (!$.isFunction(text)) {
+ dataText = data.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
+ text = function (item) { return item[dataText]; };
+ }
+ data = data.results;
+ }
+
+ return function (query) {
+ var t = query.term, filtered = { results: [] }, process;
+ if (t === "") {
+ query.callback({results: data});
+ return;
+ }
+
+ process = function(datum, collection) {
+ var group, attr;
+ datum = datum[0];
+ if (datum.children) {
+ group = {};
+ for (attr in datum) {
+ if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
+ }
+ group.children=[];
+ $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
+ if (group.children.length) {
+ collection.push(group);
+ }
+ } else {
+ if (query.matcher(t, text(datum))) {
+ collection.push(datum);
+ }
+ }
+ };
+
+ $(data).each2(function(i, datum) { process(datum, filtered.results); });
+ query.callback(filtered);
+ };
+ }
+
+ // TODO javadoc
+ function tags(data) {
+ // TODO even for a function we should probably return a wrapper that does the same object/string check as
+ // the function for arrays. otherwise only functions that return objects are supported.
+ if ($.isFunction(data)) {
+ return data;
+ }
+
+ // if not a function we assume it to be an array
+
+ return function (query) {
+ var t = query.term, filtered = {results: []};
+ $(data).each(function () {
+ var isObject = this.text !== undefined,
+ text = isObject ? this.text : this;
+ if (t === "" || query.matcher(t, text)) {
+ filtered.results.push(isObject ? this : {id: this, text: this});
+ }
+ });
+ query.callback(filtered);
+ };
+ }
+
+ /**
+ * Checks if the formatter function should be used.
+ *
+ * Throws an error if it is not a function. Returns true if it should be used,
+ * false if no formatting should be performed.
+ *
+ * @param formatter
+ */
+ function checkFormatter(formatter, formatterName) {
+ if ($.isFunction(formatter)) return true;
+ if (!formatter) return false;
+ throw new Error("formatterName must be a function or a falsy value");
+ }
+
+ function evaluate(val) {
+ return $.isFunction(val) ? val() : val;
+ }
+
+ function countResults(results) {
+ var count = 0;
+ $.each(results, function(i, item) {
+ if (item.children) {
+ count += countResults(item.children);
+ } else {
+ count++;
+ }
+ });
+ return count;
+ }
+
+ /**
+ * Default tokenizer. This function uses breaks the input on substring match of any string from the
+ * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
+ * two options have to be defined in order for the tokenizer to work.
+ *
+ * @param input text user has typed so far or pasted into the search field
+ * @param selection currently selected choices
+ * @param selectCallback function(choice) callback tho add the choice to selection
+ * @param opts select2's opts
+ * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
+ */
+ function defaultTokenizer(input, selection, selectCallback, opts) {
+ var original = input, // store the original so we can compare and know if we need to tell the search to update its text
+ dupe = false, // check for whether a token we extracted represents a duplicate selected choice
+ token, // token
+ index, // position at which the separator was found
+ i, l, // looping variables
+ separator; // the matched separator
+
+ if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
+
+ while (true) {
+ index = -1;
+
+ for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
+ separator = opts.tokenSeparators[i];
+ index = input.indexOf(separator);
+ if (index >= 0) break;
+ }
+
+ if (index < 0) break; // did not find any token separator in the input string, bail
+
+ token = input.substring(0, index);
+ input = input.substring(index + separator.length);
+
+ if (token.length > 0) {
+ token = opts.createSearchChoice(token, selection);
+ if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
+ dupe = false;
+ for (i = 0, l = selection.length; i < l; i++) {
+ if (equal(opts.id(token), opts.id(selection[i]))) {
+ dupe = true; break;
+ }
+ }
+
+ if (!dupe) selectCallback(token);
+ }
+ }
+ }
+
+ if (original.localeCompare(input) != 0) return input;
+ }
+
+ /**
+ * blurs any Select2 container that has focus when an element outside them was clicked or received focus
+ *
+ * also takes care of clicks on label tags that point to the source element
+ */
+ $(document).ready(function () {
+ $(document).delegate("*", "mousedown touchend", function (e) {
+ var target = $(e.target).closest("div.select2-container").get(0), attr;
+ if (target) {
+ $(document).find("div.select2-container-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ } else {
+ target = $(e.target).closest("div.select2-drop").get(0);
+ $(document).find("div.select2-drop-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ }
+
+ target=$(e.target);
+ attr = target.attr("for");
+ if ("LABEL" === e.target.tagName && attr && attr.length > 0) {
+ target = $("#"+attr);
+ target = target.data("select2");
+ if (target !== undefined) { target.focus(); e.preventDefault();}
+ }
+ });
+ });
+
+ /**
+ * Creates a new class
+ *
+ * @param superClass
+ * @param methods
+ */
+ function clazz(SuperClass, methods) {
+ var constructor = function () {};
+ constructor.prototype = new SuperClass;
+ constructor.prototype.constructor = constructor;
+ constructor.prototype.parent = SuperClass.prototype;
+ constructor.prototype = $.extend(constructor.prototype, methods);
+ return constructor;
+ }
+
+ AbstractSelect2 = clazz(Object, {
+
+ // abstract
+ bind: function (func) {
+ var self = this;
+ return function () {
+ func.apply(self, arguments);
+ };
+ },
+
+ // abstract
+ init: function (opts) {
+ var results, search, resultsSelector = ".select2-results";
+
+ // prepare options
+ this.opts = opts = this.prepareOpts(opts);
+
+ this.id=opts.id;
+
+ // destroy if called on an existing component
+ if (opts.element.data("select2") !== undefined &&
+ opts.element.data("select2") !== null) {
+ this.destroy();
+ }
+
+ this.enabled=true;
+ this.container = this.createContainer();
+
+ this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
+ this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
+ this.container.attr("id", this.containerId);
+
+ // cache the body so future lookups are cheap
+ this.body = thunk(function() { return opts.element.closest("body"); });
+
+ if (opts.element.attr("class") !== undefined) {
+ this.container.addClass(opts.element.attr("class").replace(/validate\[[\S ]+] ?/, ''));
+ }
+
+ this.container.css(evaluate(opts.containerCss));
+ this.container.addClass(evaluate(opts.containerCssClass));
+
+ // swap container for the element
+ this.opts.element
+ .data("select2", this)
+ .hide()
+ .before(this.container);
+ this.container.data("select2", this);
+
+ this.dropdown = this.container.find(".select2-drop");
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass));
+ this.dropdown.data("select2", this);
+
+ this.results = results = this.container.find(resultsSelector);
+ this.search = search = this.container.find("input.select2-input");
+
+ search.attr("tabIndex", this.opts.element.attr("tabIndex"));
+
+ this.resultsPage = 0;
+ this.context = null;
+
+ // initialize the container
+ this.initContainer();
+ this.initContainerWidth();
+
+ installFilteredMouseMove(this.results);
+ this.dropdown.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
+
+ installDebouncedScroll(80, this.results);
+ this.dropdown.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
+
+ // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
+ if ($.fn.mousewheel) {
+ results.mousewheel(function (e, delta, deltaX, deltaY) {
+ var top = results.scrollTop(), height;
+ if (deltaY > 0 && top - deltaY <= 0) {
+ results.scrollTop(0);
+ killEvent(e);
+ } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
+ results.scrollTop(results.get(0).scrollHeight - results.height());
+ killEvent(e);
+ }
+ });
+ }
+
+ installKeyUpChangeEvent(search);
+ search.bind("keyup-change", this.bind(this.updateResults));
+ search.bind("focus", function () { search.addClass("select2-focused"); if (search.val() === " ") search.val(""); });
+ search.bind("blur", function () { search.removeClass("select2-focused");});
+
+ this.dropdown.delegate(resultsSelector, "mouseup", this.bind(function (e) {
+ if ($(e.target).closest(".select2-result-selectable:not(.select2-disabled)").length > 0) {
+ this.highlightUnderEvent(e);
+ this.selectHighlighted(e);
+ } else {
+ this.focusSearch();
+ }
+ killEvent(e);
+ }));
+
+ // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
+ // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
+ // dom it will trigger the popup close, which is not what we want
+ this.dropdown.bind("click mouseup mousedown", function (e) { e.stopPropagation(); });
+
+ if ($.isFunction(this.opts.initSelection)) {
+ // initialize selection based on the current value of the source element
+ this.initSelection();
+
+ // if the user has provided a function that can set selection based on the value of the source element
+ // we monitor the change event on the element and trigger it, allowing for two way synchronization
+ this.monitorSource();
+ }
+
+ if (opts.element.is(":disabled") || opts.element.is("[readonly='readonly']")) this.disable();
+ },
+
+ // abstract
+ destroy: function () {
+ var select2 = this.opts.element.data("select2");
+ if (select2 !== undefined) {
+ select2.container.remove();
+ select2.dropdown.remove();
+ select2.opts.element
+ .removeData("select2")
+ .unbind(".select2")
+ .show();
+ }
+ },
+
+ // abstract
+ prepareOpts: function (opts) {
+ var element, select, idKey, ajaxUrl;
+
+ element = opts.element;
+
+ if (element.get(0).tagName.toLowerCase() === "select") {
+ this.select = select = opts.element;
+ }
+
+ if (select) {
+ // these options are not allowed when attached to a select because they are picked up off the element itself
+ $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
+ if (this in opts) {
+ throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a
+
+
+
+
+
+
+
+
+
diff --git a/addons/base_vat/i18n/es_MX.po b/addons/base_vat/i18n/es_MX.po
index c3b0b12b4b7..cd511c528b0 100644
--- a/addons/base_vat/i18n/es_MX.po
+++ b/addons/base_vat/i18n/es_MX.po
@@ -14,7 +14,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-09-07 04:57+0000\n"
+"X-Launchpad-Export-Date: 2012-09-08 04:54+0000\n"
"X-Generator: Launchpad (build 15914)\n"
#. module: base_vat
diff --git a/addons/board/static/src/css/dashboard.css b/addons/board/static/src/css/dashboard.css
index 38e224daf42..ed66e338c81 100644
--- a/addons/board/static/src/css/dashboard.css
+++ b/addons/board/static/src/css/dashboard.css
@@ -1,3 +1,17 @@
+.openerp .oe_dashboard_layout_selector ul {
+ white-space: nowrap;
+}
+.openerp .oe_dashboard_layout_selector li {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ float: left;
+}
+.openerp .oe_dashboard_layout_selector li .oe_dashboard_selected_layout {
+ margin-left: -30px;
+ vertical-align: bottom;
+ margin-bottom: 10px;
+}
.openerp .oe_dashboard_links {
text-align: right;
margin: 0 4px 6px 0;
@@ -5,11 +19,6 @@
.openerp .oe_dashboard {
width: 100%;
}
-.openerp .oe_dashboard .oe_dashboard_layout_selector ul, .openerp .oe_dashboard .oe_dashboard_layout_selector li {
- margin: 0;
- padding: 0;
- list-style-type: none;
-}
.openerp .oe_dashboard .oe_action {
margin: 0 8px 8px 0;
background-color: white;
diff --git a/addons/board/static/src/css/dashboard.sass b/addons/board/static/src/css/dashboard.sass
index 1bfc01a3809..32f0e3b4f3a 100644
--- a/addons/board/static/src/css/dashboard.sass
+++ b/addons/board/static/src/css/dashboard.sass
@@ -9,16 +9,23 @@
box-shadow: $bsval
.openerp
+ .oe_dashboard_layout_selector
+ ul
+ white-space: nowrap
+ li
+ margin: 0
+ padding: 0
+ list-style-type: none
+ float: left
+ .oe_dashboard_selected_layout
+ margin-left: -30px
+ vertical-align: bottom
+ margin-bottom: 10px
.oe_dashboard_links
text-align: right
margin: 0 4px 6px 0
.oe_dashboard
width: 100%
- .oe_dashboard_layout_selector
- ul, li
- margin: 0
- padding: 0
- list-style-type: none
.oe_action
margin: 0 8px 8px 0
background-color: white
diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js
index 0333690aa57..9c37924144d 100644
--- a/addons/board/static/src/js/dashboard.js
+++ b/addons/board/static/src/js/dashboard.js
@@ -220,36 +220,40 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({
am.do_action = function (action) {
self.do_action(action);
};
- if (action_attrs.creatable && action_attrs.creatable !== 'false') {
- var action_id = parseInt(action_attrs.creatable, 10);
- $action.parent().find('button.oe_dashboard_button_create').click(function() {
- if (isNaN(action_id)) {
- action_orig.flags.default_view = 'form';
- self.do_action(action_orig);
- } else {
- self.rpc('/web/action/load', {
- action_id: action_id
- }, function(result) {
- result.result.flags = result.result.flags || {};
- result.result.flags.default_view = 'form';
- self.do_action(result.result);
- });
- }
- });
- }
if (am.inner_widget) {
- am.inner_widget.on_mode_switch.add(function(mode) {
+ var new_form_action = function(id, editable) {
var new_views = [];
_.each(action_orig.views, function(view) {
- new_views[view[1] === mode ? 'unshift' : 'push'](view);
+ new_views[view[1] === 'form' ? 'unshift' : 'push'](view);
});
- if (!new_views.length || new_views[0][1] !== mode) {
- new_views.unshift([false, mode]);
+ if (!new_views.length || new_views[0][1] !== 'form') {
+ new_views.unshift([false, 'form']);
}
action_orig.views = new_views;
- action_orig.res_id = am.inner_widget.dataset.ids[am.inner_widget.dataset.index];
+ action_orig.res_id = id;
+ action_orig.flags = {
+ form: {
+ "initial_mode": editable ? "edit" : "view",
+ }
+ };
self.do_action(action_orig);
- });
+ };
+ var list = am.inner_widget.views.list;
+ if (list) {
+ list.deferred.then(function() {
+ $(list.controller.groups).off('row_link').on('row_link', function(e, id) {
+ new_form_action(id);
+ });
+ });
+ }
+ var kanban = am.inner_widget.views.kanban;
+ if (kanban) {
+ kanban.deferred.then(function() {
+ kanban.controller.open_record = function(id, editable) {
+ new_form_action(id, editable);
+ };
+ });
+ }
}
},
renderElement: function() {
diff --git a/addons/board/static/src/xml/board.xml b/addons/board/static/src/xml/board.xml
index b2363cafd53..e13fa707776 100644
--- a/addons/board/static/src/xml/board.xml
+++ b/addons/board/static/src/xml/board.xml
@@ -26,7 +26,6 @@
-
diff --git a/addons/crm/crm.py b/addons/crm/crm.py
index e79aeb26d75..330bbb1fc9b 100644
--- a/addons/crm/crm.py
+++ b/addons/crm/crm.py
@@ -75,7 +75,9 @@ class crm_case_stage(osv.osv):
'requirements': fields.text('Requirements'),
'section_ids':fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
- 'state': fields.selection(AVAILABLE_STATES, 'State', required=True, help="The related state for the stage. The state of your document will automatically change regarding the selected stage. For example, if a stage is related to the state 'Close', when your document reaches this stage, it will be automatically have the 'closed' state."),
+ 'state': fields.selection(AVAILABLE_STATES, 'Related Status', required=True,
+ help="The status of your document will automatically change regarding the selected stage. " \
+ "For example, if a stage is related to the state 'Close', when your document reaches this stage, it is automatically closed."),
'case_default': fields.boolean('Common to All Teams',
help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
'fold': fields.boolean('Hide in Views when Empty',
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index 29299373d91..f018a355579 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -423,7 +423,7 @@
-
+
-
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml
index 5e4c3052858..d46a0f0ba19 100644
--- a/addons/hr/security/hr_security.xml
+++ b/addons/hr/security/hr_security.xml
@@ -2,13 +2,19 @@
+
+ the user will be able to manage his own human resources stuff (leave request, timesheets, ...), if he is linked to an employee in the system.
+
+
Officer
+ the user will be able to approve document created by employees.Manager
+ the user will have an access to the human resources configuration as well as statistic reports.
diff --git a/addons/hr_attendance/hr_attendance.py b/addons/hr_attendance/hr_attendance.py
index ae05a6143fd..d3bc79bda1a 100644
--- a/addons/hr_attendance/hr_attendance.py
+++ b/addons/hr_attendance/hr_attendance.py
@@ -112,9 +112,23 @@ class hr_employee(osv.osv):
for res in cr.fetchall():
result[res[1]] = res[0] == 'sign_in' and 'present' or 'absent'
return result
+
+ def _last_sign(self, cr, uid, ids, name, args, context=None):
+ result = {}
+ if not ids:
+ return result
+ for id in ids:
+ result[id] = False
+ cr.execute("""select max(name) as name
+ from hr_attendance
+ where action in ('sign_in', 'sign_out') and employee_id = %s""",(id,))
+ for res in cr.fetchall():
+ result[id] = res[0]
+ return result
_columns = {
'state': fields.function(_state, type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Attendance'),
+ 'last_sign': fields.function(_last_sign, type='datetime', string='Last Sign'),
}
def _action_check(self, cr, uid, emp_id, dt=False, context=None):
diff --git a/addons/hr_attendance/hr_attendance_view.xml b/addons/hr_attendance/hr_attendance_view.xml
index 097d7f32258..c686b4c1ff0 100644
--- a/addons/hr_attendance/hr_attendance_view.xml
+++ b/addons/hr_attendance/hr_attendance_view.xml
@@ -127,8 +127,8 @@
-
-
-
+
+
diff --git a/addons/hr_attendance/i18n/nb.po b/addons/hr_attendance/i18n/nb.po
new file mode 100644
index 00000000000..97c2618a419
--- /dev/null
+++ b/addons/hr_attendance/i18n/nb.po
@@ -0,0 +1,576 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-10 13:06+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-11 04:50+0000\n"
+"X-Generator: Launchpad (build 15924)\n"
+
+#. module: hr_attendance
+#: model:ir.ui.menu,name:hr_attendance.menu_hr_time_tracking
+msgid "Time Tracking"
+msgstr "Tid Sporing"
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+msgid "Group By..."
+msgstr "Grupper etter ..."
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+msgid "Today"
+msgstr "I dag"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "March"
+msgstr "Mars"
+
+#. module: hr_attendance
+#: view:hr.sign.in.out.ask:0
+msgid ""
+"You did not sign out the last time. Please enter the date and time you "
+"signed out."
+msgstr ""
+"Du logget ikke ut siste gang. Vennligst skriv inn datoen og klokkeslettet du "
+"logget ut."
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Total period:"
+msgstr "Total periode:"
+
+#. module: hr_attendance
+#: field:hr.action.reason,name:0
+msgid "Reason"
+msgstr "Grunn"
+
+#. module: hr_attendance
+#: view:hr.attendance.error:0
+msgid "Print Attendance Report Error"
+msgstr "Skriv ut Oppmøte Rapporter Feil."
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:161
+#, python-format
+msgid "The sign-out date must be in the past"
+msgstr "Logg - ut må være i fortiden."
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Date Signed"
+msgstr "Signeringsdato"
+
+#. module: hr_attendance
+#: model:ir.actions.act_window,help:hr_attendance.open_view_attendance
+msgid ""
+"The Time Tracking functionality aims to manage employee attendances from "
+"Sign in/Sign out actions. You can also link this feature to an attendance "
+"device using OpenERP's web service features."
+msgstr ""
+"Tid Sporing funksjonalitet mål er å forvalte de ansattes tilskuertall fra "
+"Logg inn / Logg ut handlinger. Du kan også koble denne funksjonen til en "
+"frammøte enheten ved hjelp OpenERP web-service funksjoner."
+
+#. module: hr_attendance
+#: view:hr.action.reason:0
+msgid "Attendance reasons"
+msgstr "Oppmøte grunner"
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+#: field:hr.attendance,day:0
+msgid "Day"
+msgstr "Dag"
+
+#. module: hr_attendance
+#: selection:hr.employee,state:0
+msgid "Present"
+msgstr "Presentere"
+
+#. module: hr_attendance
+#: model:ir.model,name:hr_attendance.model_hr_sign_in_out_ask
+msgid "Ask for Sign In Out"
+msgstr "Spør for pålogging ut."
+
+#. module: hr_attendance
+#: field:hr.attendance,action_desc:0
+#: model:ir.model,name:hr_attendance.model_hr_action_reason
+msgid "Action Reason"
+msgstr "Handling grunn."
+
+#. module: hr_attendance
+#: view:hr.sign.in.out:0
+msgid "Ok"
+msgstr "Ok"
+
+#. module: hr_attendance
+#: view:hr.action.reason:0
+msgid "Define attendance reason"
+msgstr "Definer frammøte grunn."
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/report/attendance_by_month.py:184
+#: model:ir.actions.act_window,name:hr_attendance.action_hr_attendance_month
+#, python-format
+msgid "Attendances By Month"
+msgstr "Tilskuertall Pr.mnd"
+
+#. module: hr_attendance
+#: field:hr.sign.in.out,name:0
+#: field:hr.sign.in.out.ask,name:0
+msgid "Employees name"
+msgstr "Ansattnavn"
+
+#. module: hr_attendance
+#: model:ir.actions.act_window,name:hr_attendance.open_view_attendance_reason
+#: model:ir.ui.menu,name:hr_attendance.menu_open_view_attendance_reason
+msgid "Attendance Reasons"
+msgstr "Oppmøte grunner"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:161
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:167
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:174
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:179
+#, python-format
+msgid "UserError"
+msgstr "Brukerfeil"
+
+#. module: hr_attendance
+#: field:hr.attendance.error,end_date:0
+#: field:hr.attendance.week,end_date:0
+msgid "Ending Date"
+msgstr "Sluttdato"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/hr_attendance.py:140
+#, python-format
+msgid "Warning"
+msgstr "Advarsel"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:174
+#, python-format
+msgid "The Sign-in date must be in the past"
+msgstr "Logg inn dato må være i fortiden."
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:167
+#, python-format
+msgid "A sign-in must be right after a sign-out !"
+msgstr "En pålogging må være rett etter en logg ut!"
+
+#. module: hr_attendance
+#: field:hr.employee,state:0
+#: model:ir.model,name:hr_attendance.model_hr_attendance
+msgid "Attendance"
+msgstr "Oppmøte"
+
+#. module: hr_attendance
+#: field:hr.attendance.error,max_delay:0
+msgid "Max. Delay (Min)"
+msgstr "Maks. Forsinkelse (Min)"
+
+#. module: hr_attendance
+#: view:hr.attendance.error:0
+#: view:hr.attendance.month:0
+#: view:hr.attendance.week:0
+msgid "Print"
+msgstr "Skriv ut"
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+msgid "Hr Attendance Search"
+msgstr "Hr Oppmøte Søk."
+
+#. module: hr_attendance
+#: model:ir.actions.act_window,name:hr_attendance.action_hr_attendance_week
+msgid "Attendances By Week"
+msgstr "Tilskuertall etter uke."
+
+#. module: hr_attendance
+#: constraint:hr.attendance:0
+msgid "Error: Sign in (resp. Sign out) must follow Sign out (resp. Sign in)"
+msgstr "Feil: Logg på (hhv. Logg ut) må følge Logg ut (hhv. Logg inn)"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "July"
+msgstr "juli"
+
+#. module: hr_attendance
+#: model:ir.actions.act_window,name:hr_attendance.action_hr_attendance_error
+#: model:ir.actions.report.xml,name:hr_attendance.attendance_error_report
+msgid "Attendance Error Report"
+msgstr "Oppmøte Feilrapport"
+
+#. module: hr_attendance
+#: field:hr.attendance.error,init_date:0
+#: field:hr.attendance.week,init_date:0
+msgid "Starting Date"
+msgstr "Startdato"
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Min Delay"
+msgstr "Minimum forsinkelse."
+
+#. module: hr_attendance
+#: selection:hr.attendance,action:0
+#: view:hr.employee:0
+msgid "Sign In"
+msgstr "Logg Inn"
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Operation"
+msgstr "Handling"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_error.py:49
+#, python-format
+msgid "No Data Available"
+msgstr "Ingen informasjon tilgjengelig"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "September"
+msgstr "September"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "December"
+msgstr "Desember"
+
+#. module: hr_attendance
+#: field:hr.attendance.month,month:0
+msgid "Month"
+msgstr "Måned"
+
+#. module: hr_attendance
+#: field:hr.action.reason,action_type:0
+msgid "Action Type"
+msgstr "Handlingstype"
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid ""
+"(*) A negative delay means that the employee worked more than encoded."
+msgstr ""
+"(*) En negativ forsinkelse innebærer at arbeidstakeren jobbet mer enn kodet."
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+msgid "My Attendance"
+msgstr "Min Oppmøte"
+
+#. module: hr_attendance
+#: help:hr.attendance,action_desc:0
+msgid ""
+"Specifies the reason for Signing In/Signing Out in case of extra hours."
+msgstr "Angir årsaken til pålogging / logge ut I tilfelle ekstra timer."
+
+#. module: hr_attendance
+#: model:ir.model,name:hr_attendance.model_hr_attendance_month
+msgid "Print Monthly Attendance Report"
+msgstr "Skriv ut månedlig Oppmøte Rapporter."
+
+#. module: hr_attendance
+#: model:ir.model,name:hr_attendance.model_hr_sign_in_out
+msgid "Sign In Sign Out"
+msgstr "Logg inn - Logg ut"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:105
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:129
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:146
+#: view:hr.sign.in.out:0
+#: model:ir.actions.act_window,name:hr_attendance.action_hr_attendance_sigh_in_out
+#: model:ir.ui.menu,name:hr_attendance.menu_hr_attendance_sigh_in_out
+#, python-format
+msgid "Sign in / Sign out"
+msgstr "Logg inn / Logg ut"
+
+#. module: hr_attendance
+#: view:hr.sign.in.out.ask:0
+msgid "hr.sign.out.ask"
+msgstr "hr.signere.ut.spør."
+
+#. module: hr_attendance
+#: view:hr.attendance.week:0
+msgid "Print Attendance Report Weekly"
+msgstr "Skriv ut Oppmøte Rapporter Ukentlig."
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "August"
+msgstr "August"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_sign_in_out.py:179
+#, python-format
+msgid "A sign-out must be right after a sign-in !"
+msgstr "Et logg-ut må være rett etter en logg - inn!"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "June"
+msgstr "Juni"
+
+#. module: hr_attendance
+#: model:ir.model,name:hr_attendance.model_hr_attendance_error
+msgid "Print Error Attendance Report"
+msgstr "Print Feil Oppmøte Rapporter."
+
+#. module: hr_attendance
+#: field:hr.attendance,name:0
+msgid "Date"
+msgstr "Dato"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "November"
+msgstr "November"
+
+#. module: hr_attendance
+#: constraint:hr.employee:0
+msgid "Error ! You cannot create recursive Hierarchy of Employees."
+msgstr "Feil! Du kan ikke opprette rekursiv Hierarki av ansatte."
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "October"
+msgstr "Oktober"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "January"
+msgstr "Januar"
+
+#. module: hr_attendance
+#: selection:hr.action.reason,action_type:0
+#: view:hr.sign.in.out:0
+#: view:hr.sign.in.out.ask:0
+msgid "Sign in"
+msgstr "Logg inn"
+
+#. module: hr_attendance
+#: view:hr.attendance.error:0
+msgid "Analysis Information"
+msgstr "Analyse Informasjon."
+
+#. module: hr_attendance
+#: view:hr.sign.in.out:0
+msgid "Sign-out Entry Must Follow Sign-in."
+msgstr ""
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Attendance Errors"
+msgstr "Oppmøte feil"
+
+#. module: hr_attendance
+#: field:hr.attendance,action:0
+#: selection:hr.attendance,action:0
+msgid "Action"
+msgstr "Handling"
+
+#. module: hr_attendance
+#: view:hr.sign.in.out:0
+msgid ""
+"If you need your staff to sign in when they arrive at work and sign out "
+"again at the end of the day, OpenERP allows you to manage this with this "
+"tool. If each employee has been linked to a system user, then they can "
+"encode their time with this action button."
+msgstr ""
+"Hvis du trenger dine ansatte til å logge på når de kommer på jobb og logge "
+"ut igjen på slutten av dagen, kan OpenERP tillate deg å administrere dette "
+"med dette verktøyet. Hvis hver ansatt har vært knyttet til et system bruker, "
+"så de kan kode sin tid med denne handlingen knappen."
+
+#. module: hr_attendance
+#: model:ir.model,name:hr_attendance.model_hr_attendance_week
+msgid "Print Week Attendance Report"
+msgstr "Skriv Ut Uke Oppmøte Rapporter."
+
+#. module: hr_attendance
+#: field:hr.sign.in.out,emp_id:0
+#: field:hr.sign.in.out.ask,emp_id:0
+msgid "Empoyee ID"
+msgstr "Ansatt ID"
+
+#. module: hr_attendance
+#: view:hr.attendance.error:0
+#: view:hr.attendance.month:0
+#: view:hr.attendance.week:0
+#: view:hr.sign.in.out:0
+#: view:hr.sign.in.out.ask:0
+msgid "Cancel"
+msgstr "Avbryt"
+
+#. module: hr_attendance
+#: help:hr.action.reason,name:0
+msgid "Specifies the reason for Signing In/Signing Out."
+msgstr "Angir årsaken til pålogging / logger av."
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid ""
+"(*) A positive delay means that the employee worked less than recorded."
+msgstr ""
+"(*) En positiv forsinkelse innebærer at arbeidstakeren jobbet mindre enn "
+"registrert."
+
+#. module: hr_attendance
+#: view:hr.attendance.month:0
+msgid "Print Attendance Report Monthly"
+msgstr "Skriv ut Oppmøte Rapporter Månedlig."
+
+#. module: hr_attendance
+#: selection:hr.action.reason,action_type:0
+#: view:hr.sign.in.out:0
+#: view:hr.sign.in.out.ask:0
+msgid "Sign out"
+msgstr "Logg av"
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Delay"
+msgstr "Forsinkelse"
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+#: model:ir.model,name:hr_attendance.model_hr_employee
+msgid "Employee"
+msgstr "Ansatt"
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/hr_attendance.py:140
+#, python-format
+msgid ""
+"You tried to %s with a date anterior to another event !\n"
+"Try to contact the administrator to correct attendances."
+msgstr ""
+"Du prøvde å% s med en dato fremre til en annen hendelse!\n"
+"Prøv å kontakte administrator for å rette tilskuertall."
+
+#. module: hr_attendance
+#: view:hr.sign.in.out.ask:0
+#: field:hr.sign.in.out.ask,last_time:0
+msgid "Your last sign out"
+msgstr "Din siste Logg ut."
+
+#. module: hr_attendance
+#: report:report.hr.timesheet.attendance.error:0
+msgid "Date Recorded"
+msgstr "Dato Innspilt"
+
+#. module: hr_attendance
+#: model:ir.actions.act_window,name:hr_attendance.open_view_attendance
+#: model:ir.ui.menu,name:hr_attendance.menu_hr_attendance
+#: model:ir.ui.menu,name:hr_attendance.menu_open_view_attendance
+msgid "Attendances"
+msgstr "Tilskuertall"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "May"
+msgstr "Mai"
+
+#. module: hr_attendance
+#: view:hr.sign.in.out.ask:0
+msgid "Your last sign in"
+msgstr "Din siste logg inn."
+
+#. module: hr_attendance
+#: selection:hr.attendance,action:0
+#: view:hr.employee:0
+msgid "Sign Out"
+msgstr "Logg Ut"
+
+#. module: hr_attendance
+#: model:ir.actions.act_window,help:hr_attendance.action_hr_attendance_sigh_in_out
+msgid ""
+"Sign in / Sign out. In some companies, staff have to sign in when they "
+"arrive at work and sign out again at the end of the day. If each employee "
+"has been linked to a system user, then they can encode their time with this "
+"action button."
+msgstr ""
+"Logg inn / Logg ut. I noen bedrifter, ansatte må logge inn når de kommer på "
+"jobb og Logg ut igjen på slutten av dagen. Hvis hver ansatt har vært knyttet "
+"til et system bruker, så de kan kode sin tid med denne handlingen knappen."
+
+#. module: hr_attendance
+#: field:hr.attendance,employee_id:0
+msgid "Employee's Name"
+msgstr "Ansatte's navn."
+
+#. module: hr_attendance
+#: selection:hr.employee,state:0
+msgid "Absent"
+msgstr "Fraværende"
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "February"
+msgstr "Februar"
+
+#. module: hr_attendance
+#: view:hr.attendance:0
+msgid "Employee attendances"
+msgstr "Ansattes tilskuertall"
+
+#. module: hr_attendance
+#: field:hr.sign.in.out,state:0
+msgid "Current state"
+msgstr "Nåværende tilstand."
+
+#. module: hr_attendance
+#: selection:hr.attendance.month,month:0
+msgid "April"
+msgstr "April"
+
+#. module: hr_attendance
+#: view:hr.attendance.error:0
+msgid "Bellow this delay, the error is considered to be voluntary"
+msgstr "Under denne forsinkelsen, er feilen anses å være frivillig."
+
+#. module: hr_attendance
+#: code:addons/hr_attendance/wizard/hr_attendance_error.py:49
+#, python-format
+msgid "No records found for your selection!"
+msgstr "Ingen rader funnet med angitt utvalg!"
+
+#. module: hr_attendance
+#: view:hr.sign.in.out.ask:0
+msgid ""
+"You did not sign in the last time. Please enter the date and time you signed "
+"in."
+msgstr ""
+"Du hadde ikke logget ut siste gang. Vennligst skriv inn datoen og "
+"klokkeslettet du logget."
+
+#. module: hr_attendance
+#: field:hr.attendance.month,year:0
+msgid "Year"
+msgstr "År"
+
+#. module: hr_attendance
+#: view:hr.sign.in.out.ask:0
+msgid "hr.sign.in.out.ask"
+msgstr "hr.Logg.i.ut.spør"
+
+#~ msgid "Sign-Out Entry must follow Sign-In."
+#~ msgstr "Logg ut Entry må følge pålogging."
diff --git a/addons/hr_attendance/static/src/css/slider.css b/addons/hr_attendance/static/src/css/slider.css
index 9e0252156b2..664d4f96e41 100644
--- a/addons/hr_attendance/static/src/css/slider.css
+++ b/addons/hr_attendance/static/src/css/slider.css
@@ -8,9 +8,12 @@
float:left;
height: 32px;
width: 32px;
- background: url(/hr_attendance/static/src/img/emp-in32.png);
+ background: url(/hr_attendance/static/src/img/emp-out32.png);
cursor: pointer;
}
+.openerp .oe_attendance_signin:hover {
+ background: url(/hr_attendance/static/src/img/emp-out-disable32.png);
+}
.openerp .oe_attendance_status.oe_attendance_signed .oe_attendance_signin {
display: none;
}
@@ -19,9 +22,12 @@
float:right;
height: 32px;
width: 32px;
- background: url(/hr_attendance/static/src/img/emp-out32.png);
+ background: url(/hr_attendance/static/src/img/emp-in32.png);
cursor: pointer;
}
+.openerp .oe_attendance_signout:hover {
+ background: url(/hr_attendance/static/src/img/emp-in-disable32.png);
+}
.openerp .oe_attendance_status.oe_attendance_nosigned .oe_attendance_signout {
display: none;
}
diff --git a/addons/hr_attendance/static/src/js/attendance.js b/addons/hr_attendance/static/src/js/attendance.js
index f6a4c15268a..1b5e271b4ba 100644
--- a/addons/hr_attendance/static/src/js/attendance.js
+++ b/addons/hr_attendance/static/src/js/attendance.js
@@ -4,11 +4,6 @@ openerp.hr_attendance = function (instance) {
var QWeb = instance.web.qweb;
var _t = instance.web._t;
var _lt = instance.web._lt;
-
- var tips = {
- "true": _lt("You are currently signed in. Click here to sign out."),
- "false": _lt("You are currently signed out. Click here to sign in."),
- }
instance.hr_attendance.AttendanceSlider = instance.web.Widget.extend({
template: 'AttendanceSlider',
@@ -21,7 +16,6 @@ openerp.hr_attendance = function (instance) {
var tmp = function() {
this.$el.toggleClass("oe_attendance_nosigned", ! this.get("signed_in"));
this.$el.toggleClass("oe_attendance_signed", this.get("signed_in"));
- this.$el.attr("title", tips["" + this.get("signed_in")].toString());
};
this.on("change:signed_in", this, tmp);
_.bind(tmp, this)();
@@ -31,6 +25,19 @@ openerp.hr_attendance = function (instance) {
this.$(".oe_attendance_signout").click(function() {
self.do_update_attendance();
});
+ this.$el.tipsy({
+ title: function() {
+ var last_text = instance.web.format_value(self.last_sign, {type: "datetime"});
+ var current_text = instance.web.format_value(new Date(), {type: "datetime"});
+ var duration = self.last_sign ? $.timeago(self.last_sign) : "none";
+ if (self.get("signed_in")) {
+ return _.str.sprintf(_t("Last sign in: %s, %s. Click to sign out."), last_text, duration);
+ } else {
+ return _.str.sprintf(_t("Click to Sign In at %s."), current_text);
+ }
+ },
+ html: true,
+ });
return this.check_attendance();
},
do_update_attendance: function () {
@@ -39,6 +46,7 @@ openerp.hr_attendance = function (instance) {
hr_employee.call('attendance_action_change', [
[self.employee.id]
]).done(function (result) {
+ self.last_sign = new Date();
self.set({"signed_in": ! self.get("signed_in")});
});
},
@@ -49,11 +57,12 @@ openerp.hr_attendance = function (instance) {
var employee = new instance.web.DataSetSearch(self, 'hr.employee', self.session.user_context, [
['user_id', '=', self.session.uid]
]);
- return employee.read_slice(['id', 'name', 'state']).pipe(function (res) {
+ return employee.read_slice(['id', 'name', 'state', 'last_sign']).pipe(function (res) {
if (_.isEmpty(res))
return;
self.$el.show();
self.employee = res[0];
+ self.last_sign = instance.web.str_to_datetime(self.employee.last_sign);
self.set({"signed_in": self.employee.state !== "absent"});
});
},
diff --git a/addons/hr_attendance/static/src/xml/attendance.xml b/addons/hr_attendance/static/src/xml/attendance.xml
index 6896086440f..3fd9bcf2798 100644
--- a/addons/hr_attendance/static/src/xml/attendance.xml
+++ b/addons/hr_attendance/static/src/xml/attendance.xml
@@ -1,6 +1,6 @@
-
+ [[ e1[0] in ["Basic","Gross"] and e1[0] ]]
+ [[ e1[0] not in ["Basic","Gross"] and e1[0] ]]
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[1]!='' and formatLang(e1[1])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[1]!='' and formatLang(e1[1])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[2]!='' and formatLang(e1[2])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[2]!='' and formatLang(e1[2])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[3]!='' and formatLang(e1[3])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[3]!='' and formatLang(e1[3])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[4]!='' and formatLang(e1[4])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[4]!='' and formatLang(e1[4])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[5]!='' and formatLang(e1[5])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[5]!='' and formatLang(e1[5])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[6]!='' and formatLang(e1[6])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[6]!='' and formatLang(e1[6])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[7]!='' and formatLang(e1[7])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[7]!='' and formatLang(e1[7])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[8]!='' and formatLang(e1[8])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[8]!='' and formatLang(e1[8])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[9]!='' and formatLang(e1[9])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[9]!='' and formatLang(e1[9])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[10]!='' and formatLang(e1[10])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[10]!='' and formatLang(e1[10])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[11]!='' and formatLang(e1[11])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[11]!='' and formatLang(e1[11])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[12]!='' and formatLang(e1[12])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[12]!='' and formatLang(e1[12])) or removeParentNode('font') ]]
+
+
+ [[ e2[0] in ["Net"] and e2[0] ]]
+ [[ e2[0] not in ["Net"] and e2[0] ]]
+
+
+
+ [[ (e2[0] in ["Net"] and e2[1]!='' and formatLang(e2[1])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[1]!='' and formatLang(e2[1])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[2]!='' and formatLang(e2[2])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[2]!='' and formatLang(e2[2])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[3]!='' and formatLang(e2[3])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[3]!='' and formatLang(e2[3])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[4]!='' and formatLang(e2[4])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[4]!='' and formatLang(e2[4])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[5]!='' and formatLang(e2[5])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[5]!='' and formatLang(e2[5])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[6]!='' and formatLang(e2[6])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[6]!='' and formatLang(e2[6])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[7]!='' and formatLang(e2[7])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[7]!='' and formatLang(e2[7])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[8]!='' and formatLang(e2[8])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[8]!='' and formatLang(e2[8])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[9]!='' and formatLang(e2[9])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[9]!='' and formatLang(e2[9])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[10]!='' and formatLang(e2[10])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[10]!='' and formatLang(e2[10])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[11]!='' and formatLang(e2[11])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[11]!='' and formatLang(e2[11])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[12]!='' and formatLang(e2[12])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[12]!='' and formatLang(e2[12])) or removeParentNode('font') ]]
+
+
+ Payment Advice from [[ o.name ]] A/C no. [[ o.company_id.bank_ids and o.company_id.bank_ids[0].acc_number ]] form period [[ get_month(o.date)['from_name'] ]] to [[ get_month(o.date)['to_name'] ]]
+
+ Total : [[ o.line_ids==[] and removeParentNode('para') ]]
+
+
+
+ [[ o.line_ids==[] and removeParentNode('para') ]][[ formatLang(get_bysal_total()) ]] [[ (company.currency_id and company.currency_id.symbol) or '' ]]
+
+
+
+
+
+
+
+
+ [[ o.neft !=True and removeParentNode('blockTable') ]]
+
+
+
+
+
+
+
+
+
+
+
+
+ Total : [[ o.line_ids==[] and removeParentNode('para') ]]
+
+
+
+ [[ o.line_ids==[] and removeParentNode('para') ]][[ formatLang(get_bysal_total()) ]] [[ (company.currency_id and company.currency_id.symbol) or '' ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Yours Sincerely
+
+
+
+
+
+
+
+
+
+ For [[ company.name ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorized Signature
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_in_hr_payroll/report/report_payslip_details.py b/addons/l10n_in_hr_payroll/report/report_payslip_details.py
new file mode 100644
index 00000000000..2732d5d09ae
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/report/report_payslip_details.py
@@ -0,0 +1,35 @@
+#-*- coding:utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 OpenERP SA (). All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from report import report_sxw
+from hr_payroll import report
+
+class payslip_details_report_in(report.report_payslip_details.payslip_details_report):
+
+ def __init__(self, cr, uid, name, context):
+ super(payslip_details_report_in, self).__init__(cr, uid, name, context)
+ self.localcontext.update({
+ 'get_details_by_rule_category': self.get_details_by_rule_category,
+ })
+
+report_sxw.report_sxw('report.paylip.details.in', 'hr.payslip', 'l10n_in_hr_payroll/report/report_payslip_details.rml', parser=payslip_details_report_in)
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/report/report_payslip_details.rml b/addons/l10n_in_hr_payroll/report/report_payslip_details.rml
new file mode 100644
index 00000000000..a7ab3db3185
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/report/report_payslip_details.rml
@@ -0,0 +1,372 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [[repeatIn(objects,'o')]]
+
+
+
+ Pay Slip Details
+
+
+
+
+ [[o.credit_note==False and removeParentNode('para')]]
+ Credit
+
+ Note
+
+ ([[o.name or removeParentNode('para')]])
+
+
+
+ Name
+
+
+ [[o.employee_id.name]]
+
+
+ Designation
+
+
+ [[ o.employee_id.job_id.name or '' ]]
+
+
+
+
+
+
+
+ Address
+
+
+
+ [[o.employee_id.address_home_id and o.employee_id.address_home_id.name or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.street or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.street2 or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.zip or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.city or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.state_id and o.employee_id.address_home_id.state_id.name or '' ]] [[o.employee_id.address_home_id and o.employee_id.address_home_id.country_id and o.employee_id.address_home_id.country_id.name or '' ]]
+
+
+ [[ h['code'] ]]
+ [[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]
+
+
+
+ [[ '..'*h['level'] ]][[ h['rule_category'] ]][[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font') ]]
+
+
+ [[ formatLang(h['total']) ]] [[o.company_id and o.company_id.currency_id.symbol or '']] [[ h['level']==0 and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font') ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorized Signature
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_in_hr_payroll/security/ir.model.access.csv b/addons/l10n_in_hr_payroll/security/ir.model.access.csv
new file mode 100644
index 00000000000..7ca0834449b
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/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_hr_payroll_advice_user","hr.payroll.advice","model_hr_payroll_advice","base.group_hr_user",1,1,1,1
+"access_hr_payroll_advice_line_user","hr.payroll.advice.line","model_hr_payroll_advice_line","base.group_hr_user",1,1,1,1
+"access_hr_payroll_advice_report_user","payment.advice.report","model_payment_advice_report","base.group_hr_manager",1,1,1,1
+"access_hr_payroll_payslip_report_user","payslip.report","model_payslip_report","base.group_hr_manager",1,1,1,1
diff --git a/addons/l10n_in_hr_payroll/static/src/img/icon.png b/addons/l10n_in_hr_payroll/static/src/img/icon.png
new file mode 100644
index 00000000000..ddaf38f8b6d
Binary files /dev/null and b/addons/l10n_in_hr_payroll/static/src/img/icon.png differ
diff --git a/addons/l10n_in_hr_payroll/test/payment_advice.yml b/addons/l10n_in_hr_payroll/test/payment_advice.yml
new file mode 100644
index 00000000000..20b45830c5e
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/test/payment_advice.yml
@@ -0,0 +1,41 @@
+-
+ In order to test Payment Advice I create a new Payment Advice
+-
+ I create a new Payment Advice with NEFT Transaction Enable
+-
+ !record {model: hr.payroll.advice, id: payment_advice_1}:
+ name: 'NEFT Advice'
+ bank_id: base.res_bank_1
+ line_ids:
+ - employee_id: hr.employee_fp
+ name: '90125452552'
+ ifsc_code: 'abn45215145'
+ bysal: 25000.00
+ - employee_id: hr.employee_al
+ name: '00014521111232'
+ ifsc_code: 'sbi45452145'
+ bysal: 20000.00
+-
+ I check that the Payment Advice is in "Draft"
+-
+ !assert {model: hr.payroll.advice, id: payment_advice_1}:
+ - state == 'draft'
+-
+ Now I confirm Payment Advice
+-
+ !python {model: hr.payroll.advice}: |
+ self.confirm_sheet(cr, uid, [ref('payment_advice_1')])
+-
+ I check that the Payment Advice state is "Confirmed"
+-
+ !python {model: hr.payroll.advice}: |
+ advice = self.browse(cr, uid, ref("payment_advice_1"))
+ assert (advice.state == 'confirm')
+-
+ In order to test the PDF report defined on a Payment Advice, we will print a Print Advice Report when NEFT is checked
+-
+ !python {model: hr.payroll.advice}: |
+ import netsvc, tools, os
+ (data, format) = netsvc.LocalService('report.payroll.advice').create(cr, uid, [ref('l10n_in_hr_payroll.payment_advice_1')], {})
+ if tools.config['test_report_directory']:
+ file(os.path.join(tools.config['test_report_directory'], 'l10n_in_hr_payroll_summary report'+format), 'wb+').write(data)
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/test/payment_advice_batch.yml b/addons/l10n_in_hr_payroll/test/payment_advice_batch.yml
new file mode 100644
index 00000000000..d1a1436b50b
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/test/payment_advice_batch.yml
@@ -0,0 +1,61 @@
+-
+ In order to test Payment Advice Creation from Payslip Bacth I create New Payslip Batch
+-
+ I Create a bank record
+-
+ !record {model: res.partner.bank, id: res_partner_bank_0}:
+ acc_number: '3025632343043'
+ partner_id: base.res_partner_21
+ state: bank
+ bank: base.res_bank_1
+-
+ I create a new employee “Rahul”
+-
+ !record {model: hr.employee, id: hr_employee_rahul0}:
+ country_id: base.in
+ department_id: hr.dep_rd
+ name: Rahul
+ bank_account_id: res_partner_bank_0
+-
+ I want to generate a payslip from Payslip Batch.
+-
+ !record {model: hr.payslip.run, id: hr_payslip_run_forAdvice}:
+ name: Payslip Batch
+-
+ I create record for generating the Payslip for Payslip Batch.
+-
+ !record {model: hr.payslip.employees, id: hr_payslip_employees}:
+ employee_ids:
+ - hr_employee_rahul0
+-
+ I generate the payslip by clicking on Generate button wizard.
+-
+ !python {model: hr.payslip.employees}: |
+ self.compute_sheet(cr, uid, [ref('hr_payslip_employees')], context={'active_id': ref('hr_payslip_run_forAdvice')})
+-
+ I check that the Payslip Batch is in "Draft"
+-
+ !assert {model: hr.payslip.run, id: hr_payslip_run_forAdvice}:
+ - state == 'draft'
+-
+ Now I close Payslip Batch
+-
+ !python {model: hr.payslip.run}: |
+ self.close_payslip_run(cr, uid, [ref('hr_payslip_run_forAdvice')])
+-
+ I check that the Payslip Batch is "Close"
+-
+ !python {model: hr.payslip.run}: |
+ batch = self.browse(cr, uid, ref("hr_payslip_run_forAdvice"))
+ assert (batch.state == 'close')
+-
+ I create Advice from Payslip Batch using Create Advice button
+-
+ !python {model: hr.payslip.run}: |
+ self.create_advice(cr, uid, [ref('hr_payslip_run_forAdvice')])
+-
+ I check for Advice is created from Payslip Batch
+-
+ !python {model: hr.payroll.advice}: |
+ advice_ids = self.search(cr, uid, [('batch_id','=',ref('hr_payslip_run_forAdvice'))])
+ assert advice_ids,"Advice is not created from Payslip Batch."
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/wizard/__init__.py b/addons/l10n_in_hr_payroll/wizard/__init__.py
new file mode 100644
index 00000000000..36635aec3c2
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import hr_salary_employee_bymonth
+import hr_yearly_salary_detail
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth.py b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth.py
new file mode 100644
index 00000000000..25cc615a1df
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth.py
@@ -0,0 +1,71 @@
+#-*- coding:utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 OpenERP SA (). All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import time
+
+from osv import osv, fields
+
+class hr_salary_employee_bymonth(osv.osv_memory):
+
+ _name = 'hr.salary.employee.month'
+ _description = 'Hr Salary Employee By Month Report'
+ _columns = {
+ 'start_date': fields.date('Start Date', required=True),
+ 'end_date': fields.date('End Date', required=True),
+ 'employee_ids': fields.many2many('hr.employee', 'payroll_year_rel', 'payroll_year_id', 'employee_id', 'Employees', required=True),
+ 'category_id': fields.many2one('hr.salary.rule.category', 'Category', required=True),
+ }
+
+ def _get_default_category(self, cr, uid, context=None):
+ category_ids = self.pool.get('hr.salary.rule.category').search(cr, uid, [('code', '=', 'NET')], context=context)
+ return category_ids and category_ids[0] or False
+
+ _defaults = {
+ 'start_date': lambda *a: time.strftime('%Y-01-01'),
+ 'end_date': lambda *a: time.strftime('%Y-%m-%d'),
+ 'category_id': _get_default_category
+ }
+
+ def print_report(self, cr, uid, ids, context=None):
+ """
+ To get the date and print the report
+ @param self: The object pointer.
+ @param cr: A database cursor
+ @param uid: ID of the user currently logged in
+ @param context: A standard dictionary
+ @return: return report
+ """
+ if context is None:
+ context = {}
+ datas = {'ids': context.get('active_ids', [])}
+
+ res = self.read(cr, uid, ids, context=context)
+ res = res and res[0] or {}
+ datas.update({'form': res})
+ return {
+ 'type': 'ir.actions.report.xml',
+ 'report_name': 'salary.employee.bymonth',
+ 'datas': datas,
+ }
+
+hr_salary_employee_bymonth()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth_view.xml b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth_view.xml
new file mode 100644
index 00000000000..e05519b08db
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth_view.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ Hr monthly Employee Salary
+ hr.salary.employee.month
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Yearly Salary by Head
+ ir.actions.act_window
+ hr.salary.employee.month
+ form
+ form
+ new
+
+
+
+
+
+
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail.py b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail.py
new file mode 100644
index 00000000000..16bfd63bdc9
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail.py
@@ -0,0 +1,66 @@
+#-*- coding:utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 OpenERP SA (). All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import time
+
+from osv import fields, osv
+
+class yearly_salary_detail(osv.osv_memory):
+
+ _name ='yearly.salary.detail'
+ _description = 'Hr Salary Employee By Category Report'
+ _columns = {
+ 'employee_ids': fields.many2many('hr.employee', 'payroll_emp_rel', 'payroll_id', 'employee_id', 'Employees', required=True),
+ 'date_from': fields.date('Start Date', required=True),
+ 'date_to': fields.date('End Date', required=True),
+ }
+
+ _defaults = {
+ 'date_from': lambda *a: time.strftime('%Y-01-01'),
+ 'date_to': lambda *a: time.strftime('%Y-%m-%d'),
+
+ }
+
+ def print_report(self, cr, uid, ids, context=None):
+ """
+ To get the date and print the report
+ @param self: The object pointer.
+ @param cr: A database cursor
+ @param uid: ID of the user currently logged in
+ @param context: A standard dictionary
+ @return: return report
+ """
+ if context is None:
+ context = {}
+ datas = {'ids': context.get('active_ids', [])}
+
+ res = self.read(cr, uid, ids, context=context)
+ res = res and res[0] or {}
+ datas.update({'form': res})
+ return {
+ 'type': 'ir.actions.report.xml',
+ 'report_name': 'salary.detail.byyear',
+ 'datas': datas,
+ }
+
+yearly_salary_detail()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail_view.xml b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail_view.xml
new file mode 100644
index 00000000000..ec70e8b41e0
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ Employee Yearly Salary
+ yearly.salary.detail
+ form
+
+
\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
\n',
'plaintext mail incorrectly parsed')
@@ -409,12 +409,14 @@ class test_mail(common.TransactionCase):
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
# 2. Post the comment, get created message
+ parent_id = message.id
mail_compose.send_mail(cr, uid, [compose_id])
group_pigs.refresh()
message = group_pigs.message_ids[0]
- # Test: mail.message: subject as Re:.., body in html
+ # Test: mail.message: subject as Re:.., body in html, parent_id
self.assertEqual(message.subject, _msg_reply, 'mail.message incorrect subject')
self.assertIn('Administrator wrote:
Pigs rules
', message.body, 'mail.message body is incorrect')
+ self.assertEqual(message.parent_id and message.parent_id.id, parent_id, 'mail.message parent_id incorrect')
# Test: mail.message: attachments
for attach in message.attachment_ids:
self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
@@ -458,6 +460,7 @@ class test_mail(common.TransactionCase):
# It will be updated as soon as we have fixed specs !
cr, uid = self.cr, self.uid
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+
def _compare_structures(struct1, struct2, n=0):
# print '%scompare structure' % ('\t' * n)
self.assertEqual(len(struct1), len(struct2), 'message_read structure number of childs incorrect')
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 3153ccc2069..03c5a7cfe8b 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -31,6 +31,7 @@ from tools.translate import _
# main mako-like expression pattern
EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
+
class mail_compose_message(osv.TransientModel):
""" Generic message composition wizard. You may inherit from this wizard
at model and view levels to provide specific features.
@@ -117,6 +118,7 @@ class mail_compose_message(osv.TransientModel):
'body_text': lambda self, cr, uid, ctx={}: False,
'body': lambda self, cr, uid, ctx={}: '',
'subject': lambda self, cr, uid, ctx={}: False,
+ 'partner_ids': lambda self, cr, uid, ctx={}: [],
}
def notify(self, cr, uid, newid, context=None):
@@ -152,7 +154,7 @@ class mail_compose_message(osv.TransientModel):
# create subject
re_prefix = _('Re:')
reply_subject = tools.ustr(message_data.subject or '')
- if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)):
+ if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject:
reply_subject = "%s %s" % (re_prefix, reply_subject)
# create the reply in the body
reply_body = _('
On %(date)s, %(sender_name)s wrote:
%(body)s
') % {
@@ -176,8 +178,8 @@ class mail_compose_message(osv.TransientModel):
return result
def toggle_content_subtype(self, cr, uid, ids, context=None):
- """ hit toggle formatting mode button: calls onchange_formatting to
- emulate an on_change, then writes the value to update the form. """
+ """ toggle content_subtype: calls onchange_formatting to emulate an
+ on_change, then writes the value to update the form. """
for record in self.browse(cr, uid, ids, context=context):
content_st_new_value = 'plain' if record.content_subtype == 'html' else 'html'
onchange_res = self.onchange_content_subtype(cr, uid, ids, content_st_new_value, record.model, record.res_id, context=context)
@@ -185,11 +187,10 @@ class mail_compose_message(osv.TransientModel):
return True
def onchange_content_subtype(self, cr, uid, ids, value, model, res_id, context=None):
- """ onchange_content_subtype (values: 'plain' or 'html'). This onchange
- on the subtype allows to have some specific behavior when switching
- between text or html mode.
- This method can be overridden for models that want to have their
- specific behavior. """
+ """ This onchange allows to have some specific behavior when switching
+ between text or html mode. This method can be overridden.
+ :param values: 'plain' or 'html'
+ """
return {'value': {'content_subtype': value}}
def _verify_partner_email(self, cr, uid, partner_ids, context=None):
@@ -211,9 +212,9 @@ class mail_compose_message(osv.TransientModel):
}
def onchange_partner_ids(self, cr, uid, ids, value, context=None):
- """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The
- basic purpose of this method is to check that destination partners
+ """ The basic purpose of this method is to check that destination partners
effectively have email addresses. Otherwise a warning is thrown.
+ :param value: value format: [[6, 0, [3, 4]]]
"""
res = {'value': {}}
if not value or not value[0] or not value[0][0] == 6:
@@ -221,12 +222,6 @@ class mail_compose_message(osv.TransientModel):
res.update(self._verify_partner_email(cr, uid, value[0][2], context=context))
return res
- def unlink(self, cr, uid, ids, context=None):
- # Cascade delete all attachments, as they are owned by the composition wizard
- for wizard in self.read(cr, uid, ids, ['attachment_ids'], context=context):
- self.pool.get('ir.attachment').unlink(cr, uid, wizard['attachment_ids'], context=context)
- return super(mail_compose_message, self).unlink(cr, uid, ids, context=context)
-
def dummy(self, cr, uid, ids, context=None):
""" TDE: defined to have buttons that do basically nothing. It is
currently impossible to have buttons that do nothing special
@@ -255,6 +250,7 @@ class mail_compose_message(osv.TransientModel):
post_values = {
'subject': wizard.subject if wizard.content_subtype == 'html' else False,
'body': wizard.body if wizard.content_subtype == 'html' else '