diff --git a/addons/account_test/i18n/de.po b/addons/account_test/i18n/de.po new file mode 100644 index 00000000000..157223be184 --- /dev/null +++ b/addons/account_test/i18n/de.po @@ -0,0 +1,241 @@ +# German translation for openobject-addons +# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:05+0000\n" +"PO-Revision-Date: 2014-05-13 09:39+0000\n" +"Last-Translator: Claudia Haida \n" +"Language-Team: German \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2014-05-14 05:45+0000\n" +"X-Generator: Launchpad (build 17002)\n" + +#. module: account_test +#: view:accounting.assert.test:0 +msgid "" +"Code should always set a variable named `result` with the result of your " +"test, that can be a list or\n" +"a dictionary. If `result` is an empty list, it means that the test was " +"succesful. Otherwise it will\n" +"try to translate and print what is inside `result`.\n" +"\n" +"If the result of your test is a dictionary, you can set a variable named " +"`column_order` to choose in\n" +"what order you want to print `result`'s content.\n" +"\n" +"Should you need them, you can also use the following variables into your " +"code:\n" +" * cr: cursor to the database\n" +" * uid: ID of the current user\n" +"\n" +"In any ways, the code must be legal python statements with correct " +"indentation (if needed).\n" +"\n" +"Example: \n" +" sql = '''SELECT id, name, ref, date\n" +" FROM account_move_line \n" +" WHERE account_id IN (SELECT id FROM account_account WHERE type " +"= 'view')\n" +" '''\n" +" cr.execute(sql)\n" +" result = cr.dictfetchall()" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_02 +msgid "Test 2: Opening a fiscal year" +msgstr "Test2: Eröffnung eines Geschäftsjahres" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_05 +msgid "" +"Check that reconciled invoice for Sales/Purchases has reconciled entries for " +"Payable and Receivable Accounts" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_03 +msgid "" +"Check if movement lines are balanced and have the same date and period" +msgstr "" + +#. module: account_test +#: field:accounting.assert.test,name:0 +msgid "Test Name" +msgstr "" + +#. module: account_test +#: report:account.test.assert.print:0 +msgid "Accouting tests on" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_01 +msgid "Test 1: General balance" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_06 +msgid "Check that paid/reconciled invoices are not in 'Open' state" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_05_2 +msgid "" +"Check that reconciled account moves, that define Payable and Receivable " +"accounts, are belonging to reconciled invoices" +msgstr "" + +#. module: account_test +#: view:accounting.assert.test:0 +msgid "Tests" +msgstr "" + +#. module: account_test +#: field:accounting.assert.test,desc:0 +msgid "Test Description" +msgstr "" + +#. module: account_test +#: view:accounting.assert.test:0 +msgid "Description" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_06_1 +msgid "Check that there's no move for any account with « View » account type" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_08 +msgid "Test 9 : Accounts and partners on account moves" +msgstr "" + +#. module: account_test +#: model:ir.actions.act_window,name:account_test.action_accounting_assert +#: model:ir.actions.report.xml,name:account_test.account_assert_test_report +#: model:ir.ui.menu,name:account_test.menu_action_license +msgid "Accounting Tests" +msgstr "" + +#. module: account_test +#: code:addons/account_test/report/account_test_report.py:74 +#, python-format +msgid "The test was passed successfully" +msgstr "" + +#. module: account_test +#: field:accounting.assert.test,active:0 +msgid "Active" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_06 +msgid "Test 6 : Invoices status" +msgstr "" + +#. module: account_test +#: model:ir.model,name:account_test.model_accounting_assert_test +msgid "accounting.assert.test" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_05 +msgid "" +"Test 5.1 : Payable and Receivable accountant lines of reconciled invoices" +msgstr "" + +#. module: account_test +#: field:accounting.assert.test,code_exec:0 +msgid "Python code" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_07 +msgid "" +"Check on bank statement that the Closing Balance = Starting Balance + sum of " +"statement lines" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_07 +msgid "Test 8 : Closing balance on bank statements" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_03 +msgid "Test 3: Movement lines" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_05_2 +msgid "Test 5.2 : Reconcilied invoices and Payable/Receivable accounts" +msgstr "" + +#. module: account_test +#: view:accounting.assert.test:0 +msgid "Expression" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_04 +msgid "Test 4: Totally reconciled mouvements" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_04 +msgid "Check if the totally reconciled movements are balanced" +msgstr "" + +#. module: account_test +#: field:accounting.assert.test,sequence:0 +msgid "Sequence" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_02 +msgid "" +"Check if the balance of the new opened fiscal year matches with last year's " +"balance" +msgstr "" + +#. module: account_test +#: view:accounting.assert.test:0 +msgid "Python Code" +msgstr "" + +#. module: account_test +#: model:ir.actions.act_window,help:account_test.action_accounting_assert +msgid "" +"

\n" +" Click to create Accounting Test.\n" +"

\n" +" " +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_01 +msgid "Check the balance: Debit sum = Credit sum" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,desc:account_test.account_test_08 +msgid "Check that general accounts and partners on account moves are active" +msgstr "" + +#. module: account_test +#: model:accounting.assert.test,name:account_test.account_test_06_1 +msgid "Test 7: « View  » account type" +msgstr "" + +#. module: account_test +#: view:accounting.assert.test:0 +msgid "Code Help" +msgstr "" diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index c4b584100b6..46e8b024dd6 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -1,23 +1,4 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Business Applications -# Copyright (c) 2011-2014 OpenERP S.A. -# -# 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 pytz import re @@ -188,7 +169,8 @@ class calendar_attendee(osv.Model): res = cal.serialize() return res - def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False), template_xmlid='calendar_template_meeting_invitation', context=None): + def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False), + template_xmlid='calendar_template_meeting_invitation', context=None): """ Send mail for event invitation to event attendees. @param email_from: email address for user sending the mail @@ -273,7 +255,8 @@ class calendar_attendee(osv.Model): meeting_obj = self.pool['calendar.event'] res = self.write(cr, uid, ids, {'state': 'accepted'}, context) for attendee in self.browse(cr, uid, ids, context=context): - meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context) + meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)), + subtype="calendar.subtype_invitation", context=context) return res @@ -1410,7 +1393,7 @@ class calendar_event(osv.Model): new_args.append(new_arg) if not context.get('virtual_id', True): - return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count) + return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, count=count, context=context) # offset, limit, order and count must be treated separately as we may need to deal with virtual ids res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False) @@ -1535,17 +1518,17 @@ class calendar_event(osv.Model): if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True): the_id = new_id or (ids and int(ids[0])) + if the_id: + if attendees_create: + attendees_create = attendees_create[the_id] + mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids'])) + else: + mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids] - if attendees_create: - attendees_create = attendees_create[the_id] - mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids'])) - else: - mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids] - - if mail_to_ids: - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context): - self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context) + if mail_to_ids: + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) + if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context): + self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context) return res or True and False def create(self, cr, uid, vals, context=None): @@ -1624,7 +1607,7 @@ class calendar_event(osv.Model): continue if r['class'] == 'private': for f in r.keys(): - if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'): + if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date', 'rrule'): if isinstance(r[f], list): r[f] = [] else: diff --git a/addons/document/document_view.xml b/addons/document/document_view.xml index b735d2ce4a4..3376655d29d 100644 --- a/addons/document/document_view.xml +++ b/addons/document/document_view.xml @@ -243,7 +243,7 @@ ir.actions.act_window ir.attachment form - kanban,form + kanban,tree,form

Click to create a new document. diff --git a/addons/edi/i18n/ta.po b/addons/edi/i18n/ta.po new file mode 100644 index 00000000000..7f0570794ac --- /dev/null +++ b/addons/edi/i18n/ta.po @@ -0,0 +1,87 @@ +# Tamil translation for openobject-addons +# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:05+0000\n" +"PO-Revision-Date: 2014-05-13 08:27+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Tamil \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2014-05-14 05:45+0000\n" +"X-Generator: Launchpad (build 17002)\n" + +#. module: edi +#. openerp-web +#: code:addons/edi/static/src/js/edi.js:67 +#, python-format +msgid "Reason:" +msgstr "" + +#. module: edi +#. openerp-web +#: code:addons/edi/static/src/js/edi.js:60 +#, python-format +msgid "The document has been successfully imported!" +msgstr "" + +#. module: edi +#. openerp-web +#: code:addons/edi/static/src/js/edi.js:65 +#, python-format +msgid "Sorry, the document could not be imported." +msgstr "" + +#. module: edi +#: model:ir.model,name:edi.model_res_company +msgid "Companies" +msgstr "" + +#. module: edi +#: model:ir.model,name:edi.model_res_currency +msgid "Currency" +msgstr "" + +#. module: edi +#. openerp-web +#: code:addons/edi/static/src/js/edi.js:71 +#, python-format +msgid "Document Import Notification" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi.py:130 +#, python-format +msgid "Missing application." +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi.py:131 +#, python-format +msgid "" +"The document you are trying to import requires the OpenERP `%s` application. " +"You can install it by connecting as the administrator and opening the " +"configuration assistant." +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi.py:47 +#, python-format +msgid "'%s' is an invalid external ID" +msgstr "" + +#. module: edi +#: model:ir.model,name:edi.model_res_partner +msgid "Partner" +msgstr "" + +#. module: edi +#: model:ir.model,name:edi.model_edi_edi +msgid "EDI Subsystem" +msgstr "" diff --git a/addons/google_account/google_account.py b/addons/google_account/google_account.py index d42b0b67e59..7ffa4f95f48 100644 --- a/addons/google_account/google_account.py +++ b/addons/google_account/google_account.py @@ -1,28 +1,12 @@ # -*- 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 openerp +from openerp.http import request from openerp.osv import osv from openerp import SUPERUSER_ID from openerp.tools.translate import _ -from openerp.addons.web.http import request +from datetime import datetime +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT import werkzeug.urls import urllib2 @@ -31,6 +15,7 @@ import simplejson import logging _logger = logging.getLogger(__name__) + class google_service(osv.osv_memory): _name = 'google.service' @@ -48,7 +33,8 @@ class google_service(osv.osv_memory): req = urllib2.Request("https://accounts.google.com/o/oauth2/token", data, headers) content = urllib2.urlopen(req).read() except urllib2.HTTPError: - raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) + error_msg = "Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired" + raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context) content = simplejson.loads(content) return content.get('refresh_token') @@ -65,8 +51,8 @@ class google_service(osv.osv_memory): uri = 'https://accounts.google.com/o/oauth2/auth?%s' % werkzeug.url_encode(params) return uri - #If no scope is passed, we use service by default to get a default scope - def _get_authorize_uri(self, cr, uid, from_url, service, scope = False, context=None): + # If no scope is passed, we use service by default to get a default scope + def _get_authorize_uri(self, cr, uid, from_url, service, scope=False, context=None): """ This method return the url needed to allow this instance of OpenErp to access to the scope of gmail specified as parameters """ state_obj = dict(d=cr.dbname, s=service, f=from_url) @@ -76,11 +62,11 @@ class google_service(osv.osv_memory): params = { 'response_type': 'code', 'client_id': client_id, - 'state' : simplejson.dumps(state_obj), + 'state': simplejson.dumps(state_obj), 'scope': scope or 'https://www.googleapis.com/auth/%s' % (service,), 'redirect_uri': base_url + '/google_account/authentication', - 'approval_prompt':'force', - 'access_type':'offline' + 'approval_prompt': 'force', + 'access_type': 'offline' } uri = self.get_uri_oauth(a='auth') + "?%s" % werkzeug.url_encode(params) @@ -96,25 +82,24 @@ class google_service(osv.osv_memory): 'code': authorize_code, 'client_id': client_id, 'client_secret': client_secret, - 'grant_type' : 'authorization_code', + 'grant_type': 'authorization_code', 'redirect_uri': base_url + '/google_account/authentication' } headers = {"content-type": "application/x-www-form-urlencoded"} try: + uri = self.get_uri_oauth(a='token') data = werkzeug.url_encode(params) - req = urllib2.Request(self.get_uri_oauth(a='token'), data, headers) - content = urllib2.urlopen(req).read() - res = simplejson.loads(content) - except urllib2.HTTPError,e: - raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid"), context=context) + st, res = self._do_request(cr, uid, uri, params=data, headers=headers, type='POST', preuri='', context=context) + except urllib2.HTTPError: + error_msg = "Something went wrong during your token generation. Maybe your Authorization Code is invalid" + raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context) return res - def _refresh_google_token_json(self, cr, uid, refresh_token, service, context=None): #exchange_AUTHORIZATION vs Token (service = calendar) + def _refresh_google_token_json(self, cr, uid, refresh_token, service, context=None): # exchange_AUTHORIZATION vs Token (service = calendar) res = False - base_url = self.get_base_url(cr, uid, context) client_id = self.get_client_id(cr, uid, service, context) client_secret = self.get_client_secret(cr, uid, service, context) @@ -122,60 +107,79 @@ class google_service(osv.osv_memory): 'refresh_token': refresh_token, 'client_id': client_id, 'client_secret': client_secret, - 'grant_type' : 'refresh_token' + 'grant_type': 'refresh_token', } headers = {"content-type": "application/x-www-form-urlencoded"} try: - data = werkzeug.url_encode(params) - req = urllib2.Request(self.get_uri_oauth(a='token'), data, headers) - content = urllib2.urlopen(req).read() - res = simplejson.loads(content) - except urllib2.HTTPError: - raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) + uri = self.get_uri_oauth(a='token') + data = werkzeug.url_encode(params) + st, res = self._do_request(cr, uid, uri, params=data, headers=headers, type='POST', preuri='', context=context) + except urllib2.HTTPError, e: + if e.code == 400: # invalid grant + registry = openerp.modules.registry.RegistryManager.get(request.session.db) + with registry.cursor() as cur: + self.pool['res.users'].write(cur, uid, [uid], {'google_%s_rtoken' % service: False}, context=context) + error_key = simplejson.loads(e.read()).get("error", "nc") + _logger.exception("Bad google request : %s !" % error_key) + error_msg = "Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]" % error_key + raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context) return res + def _do_request(self, cr, uid, uri, params={}, headers={}, type='POST', preuri="https://www.googleapis.com", context=None): + if context is None: + context = {} - def _do_request(self,cr,uid,uri,params={},headers={},type='POST', context=None): - _logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !" % (uri,type,headers,werkzeug.url_encode(params) if type =='GET' else params)) - res = False + """ Return a tuple ('HTTP_CODE', 'HTTP_RESPONSE') """ + _logger.debug("Uri: %s - Type : %s - Headers: %s - Params : %s !" % (uri, type, headers, werkzeug.url_encode(params) if type == 'GET' else params)) + status = 418 + response = "" try: if type.upper() == 'GET' or type.upper() == 'DELETE': data = werkzeug.url_encode(params) - req = urllib2.Request(self.get_uri_api() + uri + "?" + data) - elif type.upper() == 'POST' or type.upper() == 'PATCH' or type.upper() == 'PUT': - req = urllib2.Request(self.get_uri_api() + uri, params, headers) + req = urllib2.Request(preuri + uri + "?" + data) + elif type.upper() == 'POST' or type.upper() == 'PATCH' or type.upper() == 'PUT': + req = urllib2.Request(preuri + uri, params, headers) else: raise ('Method not supported [%s] not in [GET, POST, PUT, PATCH or DELETE]!' % (type)) req.get_method = lambda: type.upper() request = urllib2.urlopen(req) + status = request.getcode() - if request.getcode() == 204: #No content returned, (ex: POST calendar/event/clear) - res = True - elif request.getcode() == 404: #Page not found - res = False + if int(status) in (204, 404): # Page not found, no response + response = False else: - content=request.read() - res = simplejson.loads(content) - except urllib2.HTTPError,e: + content = request.read() + response = simplejson.loads(content) + + if context.get('ask_time'): + try: + date = datetime.strptime(request.headers.get('date'), "%a, %d %b %Y %H:%M:%S %Z") + except: + date = datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT) + context['ask_time'] = date + except urllib2.HTTPError, e: + if e.code in (400, 401, 410): + raise e + _logger.exception("Bad google request : %s !" % e.read()) raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong with your request to google"), context=context) - return res + return (status, response) def get_base_url(self, cr, uid, context=None): - return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url',default='http://www.openerp.com?NoBaseUrl',context=context) + return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='http://www.openerp.com?NoBaseUrl', context=context) def get_client_id(self, cr, uid, service, context=None): - return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_id' % (service,),default=False,context=context) + return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_id' % (service,), default=False, context=context) def get_client_secret(self, cr, uid, service, context=None): - return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_secret' % (service,),default=False,context=context) + return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_secret' % (service,), default=False, context=context) - def get_uri_oauth(self,a=''): #a = optional action + def get_uri_oauth(self, a=''): # a = optional action return "https://accounts.google.com/o/oauth2/%s" % (a,) def get_uri_api(self): diff --git a/addons/google_calendar/__openerp__.py b/addons/google_calendar/__openerp__.py index 18f2806976b..7e7a34362f6 100644 --- a/addons/google_calendar/__openerp__.py +++ b/addons/google_calendar/__openerp__.py @@ -25,20 +25,20 @@ 'version': '1.0', 'category': 'Tools', 'description': """ -The module adds the possibility to synchronize Google Calendar with OpenERP +The module adds the possibility to synchronize Google Calendar with OpenERP ======================================== """, 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', - 'depends': ['google_account','calendar'], + 'depends': ['google_account', 'calendar'], 'qweb': ['static/src/xml/*.xml'], 'data': [ 'res_config_view.xml', 'security/ir.model.access.csv', 'views/google_calendar.xml', + 'views/res_users.xml', ], 'demo': [], 'installable': True, 'auto_install': False, } - diff --git a/addons/google_calendar/controllers/main.py b/addons/google_calendar/controllers/main.py index 49083a79bfc..1f307133ea0 100644 --- a/addons/google_calendar/controllers/main.py +++ b/addons/google_calendar/controllers/main.py @@ -1,51 +1,60 @@ -import simplejson -import urllib -import openerp import openerp.addons.web.http as http from openerp.addons.web.http import request -import openerp.addons.web.controllers.main as webmain -from openerp.addons.web.http import SessionExpiredException -from werkzeug.exceptions import BadRequest -import werkzeug.utils + class google_calendar_controller(http.Controller): - + @http.route('/google_calendar/sync_data', type='json', auth='user') - def sync_data(self, arch, fields, model,**kw): - """ + def sync_data(self, arch, fields, model, **kw): + """ This route/function is called when we want to synchronize openERP calendar with Google Calendar Function return a dictionary with the status : need_config_from_admin, need_auth, need_refresh, success if not calendar_event - The dictionary may contains an url, to allow OpenERP Client to redirect user on this URL for authorization for example + The dictionary may contains an url, to allow OpenERP Client to redirect user on this URL for authorization for example """ - + if model == 'calendar.event': gs_obj = request.registry['google.service'] gc_obj = request.registry['google.calendar'] - + # Checking that admin have already configured Google API for google synchronization ! - client_id = gs_obj.get_client_id(request.cr, request.uid,'calendar',context=kw.get('local_context')) + client_id = gs_obj.get_client_id(request.cr, request.uid, 'calendar', context=kw.get('local_context')) if not client_id or client_id == '': action = '' - if gc_obj.can_authorize_google(request.cr,request.uid): - dummy, action = request.registry.get('ir.model.data').get_object_reference(request.cr, request.uid, 'google_calendar', 'action_config_settings_google_calendar') - + if gc_obj.can_authorize_google(request.cr, request.uid): + dummy, action = request.registry.get('ir.model.data').get_object_reference(request.cr, request.uid, + 'google_calendar', 'action_config_settings_google_calendar') + return { - "status" : "need_config_from_admin", - "url" : '', - "action" : action - } - + "status": "need_config_from_admin", + "url": '', + "action": action + } + # Checking that user have already accepted OpenERP to access his calendar ! - if gc_obj.need_authorize(request.cr, request.uid,context=kw.get('local_context')): - url = gc_obj.authorize_google_uri(request.cr, request.uid, from_url=kw.get('fromurl'), context=kw.get('local_context')) + if gc_obj.need_authorize(request.cr, request.uid, context=kw.get('local_context')): + url = gc_obj.authorize_google_uri(request.cr, request.uid, from_url=kw.get('fromurl'), context=kw.get('local_context')) return { - "status" : "need_auth", - "url" : url - } - + "status": "need_auth", + "url": url + } + # If App authorized, and user access accepted, We launch the synchronization - return gc_obj.synchronize_events(request.cr, request.uid, [], kw.get('local_context')) - - return { "status" : "success" } - + return gc_obj.synchronize_events(request.cr, request.uid, [], context=kw.get('local_context')) + + return {"status": "success"} + + @http.route('/google_calendar/remove_references', type='json', auth='user') + def remove_references(self, model, **kw): + """ + This route/function is called when we want to remove all the references between one calendar OpenERP and one Google Calendar + """ + status = "NOP" + if model == 'calendar.event': + gc_obj = request.registry['google.calendar'] + # Checking that user have already accepted OpenERP to access his calendar ! + if gc_obj.remove_references(request.cr, request.uid, context=kw.get('local_context')): + status = "OK" + else: + status = "KO" + return {"status": status} diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index 177642f091d..543c81977aa 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -1,31 +1,15 @@ -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2012 OpenERP SA (). -# -# 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 . -# -############################################################################## +# -*- coding: utf-8 -*- import operator import simplejson -import urllib +import urllib2 +import openerp from openerp import tools from openerp import SUPERUSER_ID - +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.translate import _ +from openerp.http import request from datetime import datetime, timedelta from dateutil import parser import pytz @@ -35,6 +19,13 @@ import logging _logger = logging.getLogger(__name__) +def status_response(status, substr=False): + if substr: + return int(str(status)[0]) + else: + return status_response(status, substr=True) == 2 + + class Meta(type): """ This Meta class allow to define class as a structure, and so instancied variable in __init__ to avoid to have side effect alike 'static' variable """ @@ -90,7 +81,7 @@ class SyncEvent(object): def __getitem__(self, key): return getattr(self, key) - def compute_OP(self): + def compute_OP(self, modeFull=True): #If event are already in Gmail and in OpenERP if self.OE.found and self.GG.found: #If the event has been deleted from one side, we delete on other side ! @@ -120,7 +111,6 @@ class SyncEvent(object): else: if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]: self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar') - #import ipdb; ipdb.set_trace(); else: self.OP = NothingToDo("", 'Not update needed') else: @@ -128,11 +118,13 @@ class SyncEvent(object): # New in openERP... Create on create_events of synchronize function elif self.OE.found and not self.GG.found: - #Has been deleted from gmail if self.OE.status: - self.OP = Delete('OE', 'Removed from GOOGLE') + self.OP = Delete('OE', 'Update or delete from GOOGLE') else: - self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP") + if not modeFull: + self.OP = Delete('GG', 'Deleted from OpenERP, need to delete it from Gmail if already created') + else: + self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP") elif self.GG.found and not self.OE.found: tmpSrc = 'GG' if not self.GG.status and not self.GG.isInstance: @@ -151,11 +143,11 @@ class SyncEvent(object): return self.__repr__() def __repr__(self): - myPrint = "---- A SYNC EVENT ---" + myPrint = "\n\n---- A SYNC EVENT ---" myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id) myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False)) - myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name) - myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False)) + myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name.encode('utf8')) + myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', '').encode('utf8')) myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found) myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence) myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance) @@ -207,15 +199,15 @@ class google_calendar(osv.AbstractModel): STR_SERVICE = 'calendar' _name = 'google.%s' % STR_SERVICE - def generate_data(self, cr, uid, event, context=None): + def generate_data(self, cr, uid, event, isCreating=False, context=None): if event.allday: - start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0] - end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0] + start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0] + final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration) + timedelta(days=isCreating and 1 or 0), context=context).isoformat('T').split('T')[0] type = 'date' vstype = 'dateTime' else: - start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') - end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') + start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') + final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') type = 'dateTime' vstype = 'date' attendee_list = [] @@ -235,7 +227,7 @@ class google_calendar(osv.AbstractModel): 'timeZone': 'UTC' }, "end": { - type: end_date, + type: final_date, vstype: None, 'timeZone': 'UTC' }, @@ -256,10 +248,9 @@ class google_calendar(osv.AbstractModel): def create_an_event(self, cr, uid, event, context=None): gs_pool = self.pool['google.service'] + data = self.generate_data(cr, uid, event, isCreating=True, context=context) - data = self.generate_data(cr, uid, event, context=context) - - url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib.quote('id,updated'), self.get_token(cr, uid, context)) + url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib2.quote('id,updated'), self.get_token(cr, uid, context)) headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} data_json = simplejson.dumps(data) @@ -276,38 +267,89 @@ class google_calendar(osv.AbstractModel): return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context) - def get_event_dict(self, cr, uid, token=False, nextPageToken=False, context=None): + def get_calendar_primary_id(self, cr, uid, context=None): + params = { + 'fields': 'id', + 'access_token': self.get_token(cr, uid, context) + } + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + + url = "/calendar/v3/calendars/primary" + + try: + st, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context) + except Exception, e: + + if (e.code == 401): # Token invalid / Acces unauthorized + error_msg = "Your token is invalid or has been revoked !" + + registry = openerp.modules.registry.RegistryManager.get(request.session.db) + with registry.cursor() as cur: + self.pool['res.users'].write(cur, uid, [uid], {'google_calendar_token': False, 'google_calendar_token_validity': False}, context=context) + + raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context) + raise + + return status_response(st) and content['id'] or False + + def get_event_synchro_dict(self, cr, uid, lastSync=False, token=False, nextPageToken=False, context=None): if not token: token = self.get_token(cr, uid, context) - gs_pool = self.pool['google.service'] - params = { 'fields': 'items,nextPageToken', 'access_token': token, 'maxResults': 1000, - 'timeMin': self.get_start_time_to_synchro(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"), + #'timeMin': self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"), } + + if lastSync: + params['updatedMin'] = lastSync.strftime("%Y-%m-%dT%H:%M:%S.%fz") + params['showDeleted'] = True + else: + params['timeMin'] = self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz") + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} url = "/calendar/v3/calendars/%s/events" % 'primary' if nextPageToken: params['pageToken'] = nextPageToken - content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) + status, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context) google_events_dict = {} - for google_event in content['items']: google_events_dict[google_event['id']] = google_event - if content.get('nextPageToken', False): - google_events_dict.update(self.get_event_dict(cr, uid, token, content['nextPageToken'], context=context)) + if content.get('nextPageToken'): + google_events_dict.update( + self.get_event_synchro_dict(cr, uid, lastSync=lastSync, token=token, nextPageToken=content['nextPageToken'], context=context) + ) + return google_events_dict + def get_one_event_synchro(self, cr, uid, google_id, context=None): + token = self.get_token(cr, uid, context) + + params = { + 'access_token': token, + 'maxResults': 1000, + 'showDeleted': True, + } + + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + + url = "/calendar/v3/calendars/%s/events/%s" % ('primary', google_id) + try: + status, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context) + except: + _logger.info("Calendar Synchro - In except of get_one_event_synchro") + pass + + return status_response(status) and content or False + def update_to_google(self, cr, uid, oe_event, google_event, context): calendar_event = self.pool['calendar.event'] - gs_pool = self.pool['google.service'] url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context)) headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} @@ -315,7 +357,7 @@ class google_calendar(osv.AbstractModel): data['sequence'] = google_event.get('sequence', 0) data_json = simplejson.dumps(data) - content = gs_pool._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context) + status, content = self.pool['google.service']._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context) update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz") calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date}) @@ -324,15 +366,13 @@ class google_calendar(osv.AbstractModel): self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context) def update_an_event(self, cr, uid, event, context=None): - gs_pool = self.pool['google.service'] - data = self.generate_data(cr, uid, event, context=context) url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id) headers = {} data['access_token'] = self.get_token(cr, uid, context) - response = gs_pool._do_request(cr, uid, url, data, headers, type='GET', context=context) + status, response = self.pool['google.service']._do_request(cr, uid, url, data, headers, type='GET', context=context) #TO_CHECK : , if http fail, no event, do DELETE ? return response @@ -379,7 +419,12 @@ class google_calendar(osv.AbstractModel): if self.get_need_synchro_attendee(cr, uid, context=context): attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context) if not attendee_id: - attendee_id = [res_partner_obj.create(cr, uid, {'email': google_attendee['email'], 'customer': False, 'name': google_attendee.get("displayName", False) or google_attendee['email']}, context=context)] + data = { + 'email': google_attendee['email'], + 'customer': False, + 'name': google_attendee.get("displayName", False) or google_attendee['email'] + } + attendee_id = [res_partner_obj.create(cr, uid, data, context=context)] attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) partner_record.append((4, attendee.get('id'))) attendee['partner_id'] = attendee.pop('id') @@ -388,26 +433,25 @@ class google_calendar(osv.AbstractModel): UTC = pytz.timezone('UTC') if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled + if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False): date = parser.parse(single_event_dict['start']['dateTime']) - date_deadline = parser.parse(single_event_dict['end']['dateTime']) - delta = date_deadline.astimezone(UTC) - date.astimezone(UTC) + stop = parser.parse(single_event_dict['end']['dateTime']) date = str(date.astimezone(UTC))[:-6] - date_deadline = str(date_deadline.astimezone(UTC))[:-6] + stop = str(stop.astimezone(UTC))[:-6] allday = False else: - date = (single_event_dict['start']['date'] + ' 00:00:00') - date_deadline = (single_event_dict['end']['date'] + ' 00:00:00') - d_start = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") - d_end = datetime.strptime(date_deadline, "%Y-%m-%d %H:%M:%S") - delta = (d_end - d_start) + date = (single_event_dict['start']['date']) + stop = (single_event_dict['end']['date']) + d_end = datetime.strptime(stop, DEFAULT_SERVER_DATE_FORMAT) allday = True + d_end = d_end + timedelta(days=-1) + stop = d_end.strftime(DEFAULT_SERVER_DATE_FORMAT) - result['duration'] = (delta.seconds / 60) / 60.0 + delta.days * 24 update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz") result.update({ - 'date': date, - 'date_deadline': date_deadline, + 'start': date, + 'stop': stop, 'allday': allday }) result.update({ @@ -419,7 +463,6 @@ class google_calendar(osv.AbstractModel): 'location': single_event_dict.get('location', False), 'class': single_event_dict.get('visibility', 'public'), 'oe_update_date': update_date, - # 'google_internal_event_id': single_event_dict.get('id',False), }) if single_event_dict.get("recurrence", False): @@ -431,7 +474,6 @@ class google_calendar(osv.AbstractModel): elif type == "copy": result['recurrence'] = True res = calendar_event.write(cr, uid, [event['id']], result, context=context) - elif type == "create": res = calendar_event.create(cr, uid, result, context=context) @@ -439,18 +481,81 @@ class google_calendar(osv.AbstractModel): self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}, context) return res - def synchronize_events(self, cr, uid, ids, context=None): - # Create all new events from OpenERP into Gmail, if that is not recurrent event - self.create_new_events(cr, uid, context=context) - self.bind_recurring_events_to_google(cr, uid, context) - res = self.update_events(cr, uid, context) + def remove_references(self, cr, uid, context=None): + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) + reset_data = { + 'google_calendar_rtoken': False, + 'google_calendar_token': False, + 'google_calendar_token_validity': False, + 'google_calendar_last_sync_date': False, + 'google_calendar_cal_id': False, + } + all_my_attendees = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', current_user.partner_id.id)], context=context) + self.pool['calendar.attendee'].write(cr, uid, all_my_attendees, {'oe_synchro_date': False, 'google_internal_event_id': False}, context=context) + current_user.write(reset_data, context=context) + return True + + def synchronize_events(self, cr, uid, ids, lastSync=True, context=None): + if context is None: + context = {} + + # def isValidSync(syncToken): + # gs_pool = self.pool['google.service'] + # params = { + # 'maxResults': 1, + # 'fields': 'id', + # 'access_token': self.get_token(cr, uid, context), + # 'syncToken': syncToken, + # } + # url = "/calendar/v3/calendars/primary/events" + # status, response = gs_pool._do_request(cr, uid, url, params, type='GET', context=context) + # return int(status) != 410 + + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) + + context_with_time = dict(context.copy(), ask_time=True) + current_google = self.get_calendar_primary_id(cr, uid, context=context_with_time) + + if current_user.google_calendar_cal_id: + if current_google != current_user.google_calendar_cal_id: + return { + "status": "need_reset", + "info": { + "old_name": current_user.google_calendar_cal_id, + "new_name": current_google + }, + "url": '' + } + + if lastSync and self.get_last_sync_date(cr, uid, context=context) and not self.get_disable_since_synchro(cr, uid, context=context): + lastSync = self.get_last_sync_date(cr, uid, context) + _logger.info("Calendar Synchro - MODE SINCE_MODIFIED : %s !" % lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT)) + else: + lastSync = False + _logger.info("Calendar Synchro - MODE FULL SYNCHRO FORCED") + else: + current_user.write({'google_calendar_cal_id': current_google}, context=context) + lastSync = False + _logger.info("Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID") + + new_ids = [] + new_ids += self.create_new_events(cr, uid, context=context) + new_ids += self.bind_recurring_events_to_google(cr, uid, context) + + res = self.update_events(cr, uid, lastSync, context) + + current_user.write({'google_calendar_last_sync_date': context_with_time.get('ask_time')}, context=context) return { "status": res and "need_refresh" or "no_new_event_form_google", "url": '' } def create_new_events(self, cr, uid, context=None): + if context is None: + context = {} + + new_ids = [] ev_obj = self.pool['calendar.event'] att_obj = self.pool['calendar.attendee'] user_obj = self.pool['res.users'] @@ -458,32 +563,43 @@ class google_calendar(osv.AbstractModel): context_norecurrent = context.copy() context_norecurrent['virtual_id'] = False - my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False), '|', - ('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")), - ('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")), + ('event_id.stop', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + ('event_id.final_date', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ], context=context_norecurrent) - for att in att_obj.browse(cr, uid, my_att_ids, context=context): if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: - response = self.create_an_event(cr, uid, att.event_id, context=context) - update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz") - ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date}) - att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date}) - cr.commit() + st, response = self.create_an_event(cr, uid, att.event_id, context=context) + if status_response(st): + update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz") + ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date}) + new_ids.append(response['id']) + att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date}) + cr.commit() + else: + _logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st)) + _logger.warning("Response : %s" % response) + return new_ids - def bind_recurring_events_to_google(self, cr, uid, context): + def get_context_no_virtual(self, context): + context_norecurrent = context.copy() + context_norecurrent['virtual_id'] = False + context_norecurrent['active_test'] = False + return context_norecurrent + + def bind_recurring_events_to_google(self, cr, uid, context=None): + if context is None: + context = {} + + new_ids = [] ev_obj = self.pool['calendar.event'] att_obj = self.pool['calendar.attendee'] user_obj = self.pool['res.users'] myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id - context_norecurrent = context.copy() - context_norecurrent['virtual_id'] = False - context_norecurrent['active_test'] = False - + context_norecurrent = self.get_context_no_virtual(context) my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent) for att in att_obj.browse(cr, uid, my_att_ids, context=context): @@ -500,11 +616,20 @@ class google_calendar(osv.AbstractModel): if new_google_internal_event_id: #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! - self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context) - att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context) - cr.commit() + try: + st, response = self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context) + if status_response(st): + att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context) + new_ids.append(new_google_internal_event_id) + cr.commit() + else: + _logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st)) + _logger.warning("Response : %s" % response) + except: + pass + return new_ids - def update_events(self, cr, uid, context=None): + def update_events(self, cr, uid, lastSync=False, context=None): if context is None: context = {} @@ -512,20 +637,64 @@ class google_calendar(osv.AbstractModel): user_obj = self.pool['res.users'] att_obj = self.pool['calendar.attendee'] myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id + context_novirtual = self.get_context_no_virtual(context) - context_novirtual = context.copy() - context_novirtual['virtual_id'] = False - context_novirtual['active_test'] = False + if lastSync: + try: + all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=lastSync, context=context) + except urllib2.HTTPError, e: + if e.code == 410: # GONE, Google is lost. + # we need to force the rollback from this cursor, because it locks my res_users but I need to write in this tuple before to raise. + cr.rollback() + registry = openerp.modules.registry.RegistryManager.get(request.session.db) + with registry.cursor() as cur: + self.pool['res.users'].write(cur, uid, [uid], {'google_calendar_last_sync_date': False}, context=context) + error_key = simplejson.loads(e.read()) + error_key = error_key.get('error', {}).get('message', 'nc') + error_msg = "Google are lost... the next synchro will be a full synchro. \n\n %s" % error_key + raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context) - all_event_from_google = self.get_event_dict(cr, uid, context=context) + my_google_att_ids = att_obj.search(cr, uid, [ + ('partner_id', '=', myPartnerID), + ('google_internal_event_id', 'in', all_event_from_google.keys()) + ], context=context_novirtual) + + my_openerp_att_ids = att_obj.search(cr, uid, [ + ('partner_id', '=', myPartnerID), + ('event_id.oe_update_date', '>', lastSync and lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + ('google_internal_event_id', '!=', False), + ], context=context_novirtual) + + my_openerp_googleinternal_ids = att_obj.read(cr, uid, my_openerp_att_ids, ['google_internal_event_id', 'event_id'], context=context_novirtual) + + if self.get_print_log(cr, uid, context=context): + _logger.info("Calendar Synchro - \n\nUPDATE IN GOOGLE\n%s\n\nRETRIEVE FROM OE\n%s\n\nUPDATE IN OE\n%s\n\nRETRIEVE FROM GG\n%s\n\n" % (all_event_from_google, my_google_att_ids, my_openerp_att_ids, my_openerp_googleinternal_ids)) + + for giid in my_openerp_googleinternal_ids: + active = True # if not sure, we request google + if giid.get('event_id'): + active = calendar_event.browse(cr, uid, int(giid.get('event_id')[0]), context=context_novirtual).active + + if giid.get('google_internal_event_id') and not all_event_from_google.get(giid.get('google_internal_event_id')) and active: + one_event = self.get_one_event_synchro(cr, uid, giid.get('google_internal_event_id'), context=context) + if one_event: + all_event_from_google[one_event['id']] = one_event + + my_att_ids = list(set(my_google_att_ids + my_openerp_att_ids)) + + else: + domain = [ + ('partner_id', '=', myPartnerID), + ('google_internal_event_id', '!=', False), + '|', + ('event_id.stop', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + ('event_id.final_date', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + ] + + # Select all events from OpenERP which have been already synchronized in gmail + my_att_ids = att_obj.search(cr, uid, domain, context=context_novirtual) + all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=False, context=context) - # Select all events from OpenERP which have been already synchronized in gmail - my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), - ('google_internal_event_id', '!=', False), - '|', - ('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")), - ('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")), - ], context=context_novirtual) event_to_synchronize = {} for att in att_obj.browse(cr, uid, my_att_ids, context=context): event = att.event_id @@ -576,8 +745,10 @@ class google_calendar(osv.AbstractModel): ###################### for base_event in event_to_synchronize: for current_event in event_to_synchronize[base_event]: - event_to_synchronize[base_event][current_event].compute_OP() - #print event_to_synchronize[base_event] + event_to_synchronize[base_event][current_event].compute_OP(modeFull=not lastSync) + if self.get_print_log(cr, uid, context=context): + if not isinstance(event_to_synchronize[base_event][current_event].OP, NothingToDo): + _logger.info(event_to_synchronize[base_event]) ###################### # DO ACTION # @@ -615,24 +786,35 @@ class google_calendar(osv.AbstractModel): if actSrc == 'OE': self.delete_an_event(cr, uid, current_event[0], context=context) elif actSrc == 'GG': - new_google_event_id = event.GG.event['id'].split('_')[1] - if 'T' in new_google_event_id: - new_google_event_id = new_google_event_id.replace('T', '')[:-1] - else: - new_google_event_id = new_google_event_id + "000000" + new_google_event_id = event.GG.event['id'].split('_')[1] + if 'T' in new_google_event_id: + new_google_event_id = new_google_event_id.replace('T', '')[:-1] + else: + new_google_event_id = new_google_event_id + "000000" - if event.GG.status: - parent_event = {} - parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) - res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) - else: - if event_to_synchronize[base_event][0][1].OE.event_id: - parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id - calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), can_be_deleted=True, context=context) + if event.GG.status: + parent_event = {} + if not event_to_synchronize[base_event][0][1].OE.event_id: + event_to_synchronize[base_event][0][1].OE.event_id = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].split('_')[0])], ['event_id'], context=context_novirtual)[0].get('event_id')[0] + + parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) + res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) + else: + parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id + calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), can_be_deleted=True, context=context) elif isinstance(actToDo, Delete): if actSrc == 'GG': - self.delete_an_event(cr, uid, current_event[0], context=context) + try: + self.delete_an_event(cr, uid, current_event[0], context=context) + except Exception, e: + error = simplejson.loads(e.read()) + error_nr = error.get('error', {}).get('code') + # if already deleted from gmail or never created + if error_nr in (404, 410,): + pass + else: + raise e elif actSrc == 'OE': calendar_event.unlink(cr, uid, event.OE.event_id, can_be_deleted=False, context=context) return True @@ -655,7 +837,7 @@ class google_calendar(osv.AbstractModel): url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id) - content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) + st, content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) return content.get('sequence', 0) ################################# ## MANAGE CONNEXION TO GMAIL ## @@ -663,13 +845,16 @@ class google_calendar(osv.AbstractModel): def get_token(self, cr, uid, context=None): current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - - if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)): + if not current_user.google_calendar_token_validity or \ + datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) < (datetime.now() + timedelta(minutes=1)): self.do_refresh_token(cr, uid, context=context) current_user.refresh() - return current_user.google_calendar_token + def get_last_sync_date(self, cr, uid, context=None): + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) + return current_user.google_calendar_last_sync_date and datetime.strptime(current_user.google_calendar_last_sync_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(minutes=0) or False + def do_refresh_token(self, cr, uid, context=None): current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) gs_pool = self.pool['google.service'] @@ -707,14 +892,18 @@ class google_calendar(osv.AbstractModel): vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context) - def get_start_time_to_synchro(self, cr, uid, context=None): - # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 - number_of_week = 13 + def get_minTime(self, cr, uid, context=None): + number_of_week = self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.week_synchro', default=13) return datetime.now() - timedelta(weeks=number_of_week) def get_need_synchro_attendee(self, cr, uid, context=None): - # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 - return True + return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_synchro_attendee', default=True) + + def get_disable_since_synchro(self, cr, uid, context=None): + return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_since_synchro', default=False) + + def get_print_log(self, cr, uid, context=None): + return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.debug_print', default=False) class res_users(osv.Model): @@ -724,16 +913,22 @@ class res_users(osv.Model): 'google_calendar_rtoken': fields.char('Refresh Token'), 'google_calendar_token': fields.char('User token'), 'google_calendar_token_validity': fields.datetime('Token Validity'), + 'google_calendar_last_sync_date': fields.datetime('Last synchro date'), + 'google_calendar_cal_id': fields.char('Calendar ID', help='Last Calendar ID who has been synchronized. If it is changed, we remove \ +all links between GoogleID and OpenERP Google Internal ID') } class calendar_event(osv.Model): _inherit = "calendar.event" + def get_fields_need_update_google(self, cr, uid, context=None): + return ['name', 'description', 'allday', 'date', 'date_end', 'stop', 'attendee_ids', 'location', 'class', 'active'] + def write(self, cr, uid, ids, vals, context=None): if context is None: context = {} - sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class']) + sync_fields = set(self.get_fields_need_update_google(cr, uid, context)) if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context: vals['oe_update_date'] = datetime.now() @@ -762,7 +957,7 @@ class calendar_attendee(osv.Model): _inherit = 'calendar.attendee' _columns = { - 'google_internal_event_id': fields.char('Google Calendar Event Id', size=256), + 'google_internal_event_id': fields.char('Google Calendar Event Id'), 'oe_synchro_date': fields.datetime('OpenERP Synchro Date'), } _sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')] diff --git a/addons/google_calendar/static/src/js/calendar_sync.js b/addons/google_calendar/static/src/js/calendar_sync.js index fdc4aeffb77..fc05ca05ab0 100644 --- a/addons/google_calendar/static/src/js/calendar_sync.js +++ b/addons/google_calendar/static/src/js/calendar_sync.js @@ -11,38 +11,54 @@ openerp.google_calendar = function(instance) { }); return this._super(r); }, - sync_calendar: function(res,button) { + sync_calendar: function(res, button) { var self = this; var context = instance.web.pyeval.eval('context'); //$('div.oe_cal_sync_button').hide(); - $('div.oe_cal_sync_button').prop('disabled',true); - + $('div.oe_cal_sync_button').prop('disabled', true); + self.rpc('/google_calendar/sync_data', { arch: res.arch, fields: res.fields, - model:res.model, + model: res.model, fromurl: window.location.href, - local_context:context + local_context: context }).done(function(o) { - if (o.status == "need_auth") { + if (o.status === "need_auth") { alert(_t("You will be redirected on gmail to authorize your OpenErp to access your calendar !")); instance.web.redirect(o.url); } - else if (o.status == "need_config_from_admin") { - - if (!_.isUndefined(o.action) && parseInt(o.action)) { - if (confirm(_t("An admin need to configure Google Synchronization before to use it, do you want to configure it now ? !"))) { - self.do_action(o.action); + else if (o.status === "need_config_from_admin"){ + if (!_.isUndefined(o.action) && parseInt(o.action)){ + if (confirm(_t("An admin need to configure Google Synchronization before to use it, do you want to configure it now ? !"))){ + self.do_action(o.action); } } - else { + else{ alert(_t("An admin need to configure Google Synchronization before to use it !")); } } - else if (o.status == "need_refresh"){ + else if (o.status === "need_refresh"){ self.$calendar.fullCalendar('refetchEvents'); } + else if (o.status === "need_reset"){ + if (confirm(_t("The account that you are trying to synchronize (" + o.info.new_name + "), is not the same that the last one used \ +(" + o.info.old_name + "! )" + "\r\n\r\nDo you want remove all references from the old account ?"))){ + self.rpc('/google_calendar/remove_references', { + model:res.model, + local_context:context + }).done(function(o) { + if (o.status === "OK") { + alert(_t("All old references have been deleted. You can now restart the synchronization")); + } + else if (o.status === "KO") { + alert(_t("An error has occured when we was removing all old references. Please retry or contact your administrator.")); + } + //else NOP + }); + } + } }).always(function(o) { $('div.oe_cal_sync_button').prop('disabled',false); }); } }); diff --git a/addons/google_calendar/views/res_users.xml b/addons/google_calendar/views/res_users.xml new file mode 100644 index 00000000000..8adc3429090 --- /dev/null +++ b/addons/google_calendar/views/res_users.xml @@ -0,0 +1,24 @@ + + + + + res.users.form + res.users + + + + + + + + + + + + + + + + + + diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 1c4a7f55c10..6c15114cde6 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -115,7 +115,7 @@ - kanban,form + kanban,tree,form diff --git a/addons/project/project.py b/addons/project/project.py index 883db8fd0be..2249cf07dd5 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -214,7 +214,7 @@ class project(osv.osv): 'res_model': 'ir.attachment', 'type': 'ir.actions.act_window', 'view_id': False, - 'view_mode': 'kanban,form', + 'view_mode': 'kanban,tree,form', 'view_type': 'form', 'limit': 80, 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id) diff --git a/addons/sale/i18n/fr_CA.po b/addons/sale/i18n/fr_CA.po new file mode 100644 index 00000000000..867db031b73 --- /dev/null +++ b/addons/sale/i18n/fr_CA.po @@ -0,0 +1,2288 @@ +# French (Canada) translation for openobject-addons +# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:04+0000\n" +"PO-Revision-Date: 2014-05-13 02:46+0000\n" +"Last-Translator: Maxime Chambreuil (http://www.savoirfairelinux.com) " +"\n" +"Language-Team: French (Canada) \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2014-05-14 05:45+0000\n" +"X-Generator: Launchpad (build 17002)\n" + +#. module: sale +#: model:res.groups,name:sale.group_analytic_accounting +msgid "Analytic Accounting for Sales" +msgstr "" + +#. module: sale +#: model:process.transition,name:sale.process_transition_confirmquotation0 +msgid "Confirm Quotation" +msgstr "Confirmer la soumission" + +#. module: sale +#: view:board.board:0 +msgid "Sales Dashboard" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:92 +#, python-format +msgid "There is no income account defined as global property." +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_order_line_tree2 +#: model:ir.ui.menu,name:sale.menu_invoicing_sales_order_lines +msgid "Order Lines to Invoice" +msgstr "" + +#. module: sale +#: field:sale.order,date_confirm:0 +msgid "Confirmation Date" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: view:sale.order.line:0 +#: view:sale.report:0 +msgid "Group By..." +msgstr "" + +#. module: sale +#: field:sale.order.line,address_allotment_id:0 +msgid "Allotment Partner" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_view_sale_advance_payment_inv +msgid "Invoice Order" +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "Product Features" +msgstr "" + +#. module: sale +#: help:sale.config.settings,module_account_analytic_analysis:0 +msgid "" +"Allows to define your customer contracts conditions: invoicing\n" +" method (fixed price, on timesheet, advance invoice), the exact " +"pricing\n" +" (650€/day for a developer), the duration (one year support " +"contract).\n" +" You will be able to follow the progress of the contract and " +"invoice automatically.\n" +" It installs the account_analytic_analysis module." +msgstr "" + +#. module: sale +#: model:email.template,report_name:sale.email_template_edi_sale +msgid "" +"${(object.name or '').replace('/','_')}_${object.state == 'draft' and " +"'draft' or ''}" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +#: field:sale.report,product_uom:0 +msgid "Unit of Measure" +msgstr "" + +#. module: sale +#: field:sale.order.line,type:0 +msgid "Procurement Method" +msgstr "" + +#. module: sale +#: help:sale.order,date_confirm:0 +msgid "Date on which sales order is confirmed." +msgstr "" + +#. module: sale +#: field:account.config.settings,module_sale_analytic_plans:0 +msgid "Use multiple analytic accounts on sales" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "March" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:565 +#, python-format +msgid "First cancel all invoices attached to this sales order." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Quotation Number" +msgstr "Numéro de soumission" + +#. module: sale +#: view:sale.order:0 +#: field:sale.order,message_unread:0 +msgid "Unread Messages" +msgstr "" + +#. module: sale +#: field:sale.order,company_id:0 +#: field:sale.order.line,company_id:0 +#: view:sale.report:0 +#: field:sale.report,company_id:0 +#: field:sale.shop,company_id:0 +msgid "Company" +msgstr "" + +#. module: sale +#: field:sale.make.invoice,invoice_date:0 +msgid "Invoice Date" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_order_line_tree3 +msgid "Uninvoiced and Delivered Lines" +msgstr "" + +#. module: sale +#: model:mail.message.subtype,description:sale.mt_order_confirmed +msgid "Quotation confirmed" +msgstr "Soumission confirmée" + +#. module: sale +#: selection:sale.order,state:0 +#: selection:sale.report,state:0 +msgid "Invoice Exception" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Quotation " +msgstr "Soumission " + +#. module: sale +#: selection:sale.order,state:0 +msgid "Draft Quotation" +msgstr "Soumission en brouillon" + +#. module: sale +#: field:sale.order,partner_shipping_id:0 +msgid "Delivery Address" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,analytic_account_id:0 +#: field:sale.shop,project_id:0 +msgid "Analytic Account" +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_sale_journal:0 +msgid "Allow batch invoicing of delivery orders through journals" +msgstr "" + +#. module: sale +#: field:sale.order.line,price_subtotal:0 +msgid "Subtotal" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,day:0 +msgid "Day" +msgstr "" + +#. module: sale +#: model:process.transition.action,name:sale.process_transition_action_cancelorder0 +#: view:sale.order:0 +msgid "Cancel Order" +msgstr "" + +#. module: sale +#: field:sale.order.line,th_weight:0 +msgid "Weight" +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "Warehouse Features" +msgstr "" + +#. module: sale +#: field:sale.config.settings,time_unit:0 +msgid "The default working time unit for services is" +msgstr "" + +#. module: sale +#: field:sale.order.line,product_uom:0 +msgid "Unit of Measure " +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:101 +#, python-format +msgid "Incorrect Data" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:102 +#, python-format +msgid "The value of Advance Amount must be positive." +msgstr "" + +#. module: sale +#: help:sale.config.settings,group_discount_per_so_line:0 +msgid "Allows you to apply some discount per sales order line." +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Sales Order Lines that are in 'done' state" +msgstr "" + +#. module: sale +#: selection:sale.order.line,type:0 +msgid "on order" +msgstr "" + +#. module: sale +#: field:sale.order,message_ids:0 +msgid "Messages" +msgstr "" + +#. module: sale +#: field:sale.report,state:0 +msgid "Order Status" +msgstr "" + +#. module: sale +#: field:sale.order,amount_tax:0 +#: field:sale.order.line,tax_id:0 +msgid "Taxes" +msgstr "" + +#. module: sale +#: field:sale.order,amount_untaxed:0 +msgid "Untaxed Amount" +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_project:0 +msgid "Project" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:185 +#: code:addons/sale/sale.py:363 +#: code:addons/sale/sale.py:504 +#: code:addons/sale/sale.py:598 +#: code:addons/sale/sale.py:763 +#: code:addons/sale/sale.py:780 +#, python-format +msgid "Error!" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Net Total :" +msgstr "" + +#. module: sale +#: help:sale.order.line,type:0 +msgid "" +"From stock: When needed, the product is taken from the stock or we wait for " +"replenishment.\n" +"On order: When needed, the product is purchased or produced." +msgstr "" + +#. module: sale +#: help:sale.config.settings,module_analytic_user_function:0 +msgid "" +"Allows you to define what is the default function of a specific user on a " +"given account.\n" +" This is mostly used when a user encodes his timesheet. The " +"values are retrieved and the fields are auto-filled.\n" +" But the possibility to change these values is still " +"available.\n" +" This installs the module analytic_user_function." +msgstr "" + +#. module: sale +#: selection:sale.order,state:0 +#: selection:sale.order.line,state:0 +#: selection:sale.report,state:0 +msgid "Cancelled" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Sales Order Lines related to a Sales Order of mine" +msgstr "" + +#. module: sale +#: selection:sale.order,state:0 +msgid "Quotation Sent" +msgstr "Soumission envoyée" + +#. module: sale +#: model:ir.model,name:sale.model_mail_compose_message +msgid "Email composition wizard" +msgstr "" + +#. module: sale +#: help:sale.order,message_unread:0 +msgid "If checked new messages require your attention." +msgstr "" + +#. module: sale +#: selection:sale.order,state:0 +#: selection:sale.report,state:0 +msgid "Shipping Exception" +msgstr "" + +#. module: sale +#: field:sale.order.line,product_uos_qty:0 +msgid "Quantity (UoS)" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_shop_form +#: field:sale.order,shop_id:0 +#: view:sale.report:0 +#: field:sale.report,shop_id:0 +msgid "Shop" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_orders_exception +msgid "Sales in Exception" +msgstr "" + +#. module: sale +#: field:sale.order,partner_invoice_id:0 +msgid "Invoice Address" +msgstr "" + +#. module: sale +#: help:sale.order,create_date:0 +msgid "Date on which sales order is created." +msgstr "" + +#. module: sale +#: view:res.partner:0 +msgid "False" +msgstr "" + +#. module: sale +#: help:sale.advance.payment.inv,advance_payment_method:0 +msgid "" +"Use All to create the final invoice.\n" +" Use Percentage to invoice a percentage of the total amount.\n" +" Use Fixed Price to invoice a specific amound in advance.\n" +" Use Some Order Lines to invoice a selection of the sales " +"order lines." +msgstr "" + +#. module: sale +#: view:sale.make.invoice:0 +#: view:sale.order.line.make.invoice:0 +msgid "Create Invoices" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Tax" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:277 +#: code:addons/sale/sale.py:820 +#: code:addons/sale/sale.py:983 +#, python-format +msgid "Invalid Action!" +msgstr "" + +#. module: sale +#: help:sale.order,state:0 +msgid "" +"Gives the status of the quotation or sales order. \n" +"The exception status is automatically set when a cancel operation occurs " +" in the invoice validation (Invoice Exception) or in the picking " +"list process (Shipping Exception).\n" +"The 'Waiting Schedule' status is set when the invoice is confirmed " +" but waiting for the scheduler to run on the order date." +msgstr "" +"Donne le statut d'une soumission ou d'une commande de vente.\n" +"Le statut 'Exception' est automatiquement défini quand une opération " +"d'annulation est effectuée dans la validation de la facture (Exception de " +"facturation) ou dans le processus de colisage (Exception de colisage).\n" +"Le statut 'En attente' est défini quand une facture est confirmée mais on " +"attend le lancement du planificateur à la date de la commande." + +#. module: sale +#: field:sale.report,date_confirm:0 +msgid "Date Confirm" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,nbr:0 +msgid "# of Lines" +msgstr "" + +#. module: sale +#: help:sale.order,message_summary:0 +msgid "" +"Holds the Chatter summary (number of messages, ...). This summary is " +"directly in html format in order to be inserted in kanban views." +msgstr "" + +#. module: sale +#: help:sale.order.line,sequence:0 +msgid "Gives the sequence order when displaying a list of sales order lines." +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,product_uom_qty:0 +msgid "# of Qty" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Fax :" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:277 +#, python-format +msgid "" +"In order to delete a confirmed sales order, you must cancel it before !" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "(update)" +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_res_partner +#: view:sale.report:0 +#: field:sale.report,partner_id:0 +msgid "Partner" +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "Contract Features" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:287 +#: code:addons/sale/sale.py:584 +#: model:ir.model,name:sale.model_sale_order +#: model:process.node,name:sale.process_node_order0 +#: model:process.node,name:sale.process_node_saleorder0 +#: field:res.partner,sale_order_ids:0 +#: model:res.request.link,name:sale.req_link_sale_order +#: view:sale.order:0 +#: selection:sale.order,state:0 +#, python-format +msgid "Sales Order" +msgstr "" + +#. module: sale +#: model:res.groups,name:sale.group_invoice_so_lines +msgid "Enable Invoicing Sales order lines" +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale +#: field:sale.advance.payment.inv,amount:0 +msgid "Advance Amount" +msgstr "" + +#. module: sale +#: help:sale.order,invoice_exists:0 +msgid "It indicates that sales order has at least one invoice." +msgstr "" + +#. module: sale +#: help:sale.config.settings,group_sale_pricelist:0 +msgid "" +"Allows to manage different prices based on rules per category of customers.\n" +"Example: 10% for retailers, promotion of 5 EUR on this product, etc." +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_analytic_user_function:0 +msgid "One employee can have different roles per contract" +msgstr "" + +#. module: sale +#: selection:sale.advance.payment.inv,advance_payment_method:0 +msgid "Invoice the whole sales order" +msgstr "" + +#. module: sale +#: field:sale.shop,payment_default_id:0 +msgid "Default Payment Term" +msgstr "" + +#. module: sale +#: model:process.transition.action,name:sale.process_transition_action_confirm0 +msgid "Confirm" +msgstr "" + +#. module: sale +#: field:sale.config.settings,timesheet:0 +msgid "Prepare invoices based on timesheets" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:820 +#, python-format +msgid "You cannot cancel a sales order line that has already been invoiced." +msgstr "" + +#. module: sale +#: view:account.invoice.report:0 +#: view:board.board:0 +#: model:ir.actions.act_window,name:sale.action_turnover_by_month +msgid "Monthly Turnover" +msgstr "" + +#. module: sale +#: selection:sale.order,invoice_quantity:0 +msgid "Shipped Quantities" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,year:0 +msgid "Year" +msgstr "" + +#. module: sale +#: field:sale.config.settings,group_uom:0 +msgid "Allow using different units of measures" +msgstr "" + +#. module: sale +#: model:mail.message.subtype,name:sale.mt_order_confirmed +msgid "Sales Order Confirmed" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Sales Order that haven't yet been confirmed" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Print" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Order N°" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: field:sale.order,order_line:0 +msgid "Order Lines" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Disc.(%)" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:764 +#, python-format +msgid "Please define income account for this product: \"%s\" (id:%d)." +msgstr "" + +#. module: sale +#: field:sale.order.line,invoice_lines:0 +msgid "Invoice Lines" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,price_total:0 +msgid "Total Price" +msgstr "" + +#. module: sale +#: help:account.config.settings,group_analytic_account_for_sales:0 +msgid "Allows you to specify an analytic account on sales orders." +msgstr "" + +#. module: sale +#: help:sale.config.settings,module_sale_journal:0 +msgid "" +"Allows you to categorize your sales and deliveries (picking lists) between " +"different journals,\n" +" and perform batch operations on journals.\n" +" This installs the module sale_journal." +msgstr "" + +#. module: sale +#: help:sale.make.invoice,grouped:0 +msgid "Check the box to group the invoices for the same customers" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_sale_order_make_invoice +#: model:ir.actions.act_window,name:sale.action_view_sale_order_line_make_invoice +msgid "Make Invoices" +msgstr "" + +#. module: sale +#: code:addons/sale/res_config.py:97 +#, python-format +msgid "Hour" +msgstr "" + +#. module: sale +#: field:res.partner,sale_order_count:0 +msgid "# of Sales Order" +msgstr "" + +#. module: sale +#: help:sale.config.settings,timesheet:0 +msgid "" +"For modifying account analytic view to show important data to project " +"manager of services companies.\n" +" You can also view the report of account analytic summary " +"user-wise as well as month wise.\n" +" This installs the module account_analytic_analysis." +msgstr "" + +#. module: sale +#: field:sale.order,create_date:0 +msgid "Creation Date" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: view:sale.order.line:0 +msgid "To Invoice" +msgstr "" + +#. module: sale +#: help:sale.order,partner_invoice_id:0 +msgid "Invoice address for current sales order." +msgstr "" + +#. module: sale +#: selection:sale.order,invoice_quantity:0 +msgid "Ordered Quantities" +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "Ordered Year of the sales order" +msgstr "" + +#. module: sale +#: model:res.groups,name:sale.group_delivery_invoice_address +msgid "Addresses in Sales Orders" +msgstr "" + +#. module: sale +#: field:sale.advance.payment.inv,qtty:0 +#: report:sale.order:0 +#: field:sale.order.line,product_uom_qty:0 +msgid "Quantity" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Total :" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Sales Order Lines ready to be invoiced" +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "My Sales" +msgstr "" + +#. module: sale +#: field:sale.order,name:0 +#: field:sale.order.line,order_id:0 +msgid "Order Reference" +msgstr "" + +#. module: sale +#: field:sale.order,fiscal_position:0 +msgid "Fiscal Position" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "July" +msgstr "" + +#. module: sale +#: help:sale.order.line,state:0 +msgid "" +"* The 'Draft' status is set when the related sales order in draft status. " +" \n" +"* The 'Confirmed' status is set when the related sales order is confirmed. " +" \n" +"* The 'Exception' status is set when the related sales order is set as " +"exception. \n" +"* The 'Done' status is set when the sales order line has been picked. " +" \n" +"* The 'Cancelled' status is set when a user cancel the sales order related." +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "Default Options" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:960 +#: code:addons/sale/wizard/sale_make_invoice_advance.py:91 +#: code:addons/sale/wizard/sale_make_invoice_advance.py:95 +#, python-format +msgid "Configuration Error!" +msgstr "" + +#. module: sale +#: field:account.config.settings,group_analytic_account_for_sales:0 +msgid "Analytic accounting for sales" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: field:sale.order,state:0 +#: view:sale.order.line:0 +#: field:sale.order.line,state:0 +#: view:sale.report:0 +msgid "Status" +msgstr "" + +#. module: sale +#: view:sale.advance.payment.inv:0 +msgid "" +"After clicking 'Show Lines to Invoice', select lines to invoice and create " +"the invoice from the 'More' dropdown menu." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Send by Email" +msgstr "" + +#. module: sale +#: code:addons/sale/edi/sale_order.py:140 +#, python-format +msgid "EDI Pricelist (%s)" +msgstr "" + +#. module: sale +#: selection:sale.order,order_policy:0 +msgid "On Delivery Order" +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "Invoicing Process" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Order Date" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Sales Order done" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:364 +#, python-format +msgid "Please define sales journal for this company: \"%s\" (id:%d)." +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.act_res_partner_2_sale_order +#: view:res.partner:0 +msgid "Quotations and Sales" +msgstr "Soumissions et ventes" + +#. module: sale +#: field:sale.order,invoiced:0 +msgid "Paid" +msgstr "" + +#. module: sale +#: help:sale.config.settings,group_uom:0 +msgid "" +"Allows you to select and maintain different units of measure for products." +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "Reference Unit of Measure" +msgstr "" + +#. module: sale +#: view:sale.advance.payment.inv:0 +msgid "Create and View Invoice" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Sales order lines done" +msgstr "" + +#. module: sale +#: field:sale.make.invoice,grouped:0 +msgid "Group the invoices" +msgstr "" + +#. module: sale +#: help:sale.advance.payment.inv,amount:0 +msgid "The amount to be invoiced in advance." +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_make_invoice +msgid "Sales Make Invoice" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:307 +#, python-format +msgid "Pricelist Warning!" +msgstr "" + +#. module: sale +#: field:sale.order.line,discount:0 +msgid "Discount (%)" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_line_invoice.py:107 +#, python-format +msgid "" +"Invoice cannot be created for this Sales Order Line due to one of the " +"following reasons:\n" +"1.The state of this sales order line is either \"draft\" or \"cancel\"!\n" +"2.The Sales Order Line is Invoiced!" +msgstr "" + +#. module: sale +#: view:sale.order.line.make.invoice:0 +msgid "Create & View Invoice" +msgstr "" + +#. module: sale +#: view:board.board:0 +#: model:ir.actions.act_window,name:sale.action_quotation_for_sale +msgid "My Quotations" +msgstr "Mes soumissions" + +#. module: sale +#: field:sale.order,invoice_ids:0 +msgid "Invoices" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "December" +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "Contracts Management" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Shipped" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,month:0 +msgid "Month" +msgstr "" + +#. module: sale +#: field:sale.order,currency_id:0 +msgid "Currency" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Uninvoiced" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,categ_id:0 +msgid "Category of Product" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:564 +#, python-format +msgid "Cannot cancel this sales order!" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Recreate Invoice" +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_warning:0 +msgid "Allow configuring alerts by customer or products" +msgstr "" + +#. module: sale +#: field:sale.shop,name:0 +msgid "Shop Name" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "My Sales Orders" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Taxes :" +msgstr "" + +#. module: sale +#: field:sale.order,invoice_exists:0 +#: field:sale.order.line,invoiced:0 +msgid "Invoiced" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:202 +#, python-format +msgid "Advance Invoice" +msgstr "" + +#. module: sale +#: help:sale.config.settings,group_invoice_so_lines:0 +msgid "" +"To allow your salesman to make invoices for sales order lines using the menu " +"'Lines to Invoice'." +msgstr "" + +#. module: sale +#: model:ir.actions.client,name:sale.action_client_sale_menu +msgid "Open Sale Menu" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:598 +#, python-format +msgid "You cannot confirm a sales order which has no line." +msgstr "" + +#. module: sale +#: selection:sale.report,state:0 +msgid "In Progress" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:865 +#, python-format +msgid "No Customer Defined !" +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_sale_stock:0 +msgid "Trigger delivery orders automatically from sales orders" +msgstr "" + +#. module: sale +#: view:sale.make.invoice:0 +#: view:sale.order.line.make.invoice:0 +msgid "Create invoices" +msgstr "" + +#. module: sale +#: selection:sale.order.line,state:0 +msgid "Confirmed" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:106 +#, python-format +msgid "Advance of %s %%" +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_order_line_make_invoice +msgid "Sale OrderLine Make_invoice" +msgstr "" + +#. module: sale +#: selection:sale.order.line,state:0 +msgid "Draft" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_email_templates +msgid "Email Templates" +msgstr "" + +#. module: sale +#: help:sale.order.line,address_allotment_id:0 +msgid "A partner to whom the particular product needs to be allotted." +msgstr "" + +#. module: sale +#: field:sale.order,project_id:0 +msgid "Contract / Analytic" +msgstr "" + +#. module: sale +#: selection:sale.order,state:0 +#: selection:sale.report,state:0 +msgid "Waiting Schedule" +msgstr "" + +#. module: sale +#: field:sale.order,note:0 +msgid "Terms and conditions" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_orders +#: model:ir.ui.menu,name:sale.menu_sale_order +#: view:sale.order:0 +msgid "Sales Orders" +msgstr "" + +#. module: sale +#: help:sale.order,amount_tax:0 +msgid "The tax amount." +msgstr "" + +#. module: sale +#: field:sale.order,invoiced_rate:0 +msgid "Invoiced Ratio" +msgstr "" + +#. module: sale +#: selection:sale.order,order_policy:0 +msgid "On Demand" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "August" +msgstr "" + +#. module: sale +#: model:process.node,note:sale.process_node_saleorder0 +msgid "Drives procurement and invoicing" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "To Do" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "June" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,help:sale.action_shop_form +msgid "" +"

\n" +" Click to define a new sale shop.\n" +"

\n" +" Each quotation or sales order must be linked to a shop. The\n" +" shop also defines the warehouse from which the products will " +"be\n" +" delivered for each particular sales.\n" +"

\n" +" " +msgstr "" +"

\n" +" Cliquez pour définir un nouveau magasin.\n" +"

\n" +" Chaque soumission ou vente doit être liée à un magasin.\n" +" Le magasin définit également de quel entrepôt les produits " +"seront\n" +" livrés pour chaque vente.\n" +"

\n" +" " + +#. module: sale +#: view:sale.order.line:0 +msgid "Order" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_order_report_all +#: model:ir.ui.menu,name:sale.menu_report_product_all +#: view:sale.report:0 +msgid "Sales Analysis" +msgstr "" + +#. module: sale +#: field:sale.order,message_is_follower:0 +msgid "Is a Follower" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "" +"Sales Order Lines that are confirmed, done or in exception state and haven't " +"yet been invoiced" +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_report +msgid "Sales Orders Statistics" +msgstr "" + +#. module: sale +#: field:sale.order,date_order:0 +msgid "Date" +msgstr "" + +#. module: sale +#: report:sale.order:0 +#: view:sale.order:0 +#: field:sale.order,user_id:0 +#: view:sale.order.line:0 +#: field:sale.order.line,salesman_id:0 +#: view:sale.report:0 +#: field:sale.report,user_id:0 +msgid "Salesperson" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "November" +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "Extended Filters..." +msgstr "" + +#. module: sale +#: model:process.node,name:sale.process_node_quotation0 +#: view:sale.order:0 +#: selection:sale.report,state:0 +msgid "Quotation" +msgstr "Soumission" + +#. module: sale +#: field:sale.advance.payment.inv,product_id:0 +msgid "Advance Product" +msgstr "" + +#. module: sale +#: selection:sale.order.line,state:0 +msgid "Exception" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "October" +msgstr "" + +#. module: sale +#: model:process.transition,note:sale.process_transition_invoice0 +msgid "" +"The Salesman creates an invoice manually, if the sales order shipping policy " +"is 'Shipping and Manual in Progress'. The invoice is created automatically " +"if the shipping policy is 'Payment before Delivery'." +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_shop +#: view:sale.shop:0 +msgid "Sales Shop" +msgstr "" + +#. module: sale +#: help:sale.config.settings,module_sale_stock:0 +msgid "" +"Allows you to Make Quotation, Sale Order using different Order policy and " +"Manage Related Stock.\n" +" This installs the module sale_stock." +msgstr "" +"Vous permet de faire des soumissions, des commandes de vente en utilisant " +"différentes politique de commande et de gérer les stock associés.\n" +" Ceci installe le module 'sale_stock'." + +#. module: sale +#: help:sale.advance.payment.inv,product_id:0 +msgid "" +"Select a product of type service which is called 'Advance Product'.\n" +" You may have to create it and set it as a default value on " +"this field." +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "January" +msgstr "" + +#. module: sale +#: field:sale.config.settings,group_discount_per_so_line:0 +msgid "Allow setting a discount on the sales order lines" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_orders_in_progress +msgid "Sales Order in Progress" +msgstr "" + +#. module: sale +#: view:sale.order.line.make.invoice:0 +msgid "" +"All items in these order lines will be invoiced. You can also invoice a " +"percentage of the sales order\n" +" or a fixed price (for advances) directly from the sales " +"order form if you prefer." +msgstr "" + +#. module: sale +#: help:sale.config.settings,module_warning:0 +msgid "" +"Allow to configure notification on products and trigger them when a user " +"wants to sale a given product or a given customer.\n" +"Example: Product: this product is deprecated, do not purchase more than 5.\n" +" Supplier: don't forget to ask for an express delivery." +msgstr "" + +#. module: sale +#: field:sale.order,paypal_url:0 +msgid "Paypal Url" +msgstr "" + +#. module: sale +#: help:sale.order,project_id:0 +msgid "The analytic account related to a sales order." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "View Invoice" +msgstr "" + +#. module: sale +#: field:sale.advance.payment.inv,advance_payment_method:0 +msgid "What do you want to invoice?" +msgstr "" + +#. module: sale +#: field:sale.config.settings,group_sale_pricelist:0 +msgid "Use pricelists to adapt your price per customers" +msgstr "" + +#. module: sale +#: model:process.transition,note:sale.process_transition_confirmquotation0 +msgid "" +"The salesman confirms the quotation. The state of the sales order becomes " +"'In progress' or 'Manual in progress'." +msgstr "" +"Le vendeur confirme la soumission. L'état du bon de commande devient \"En " +"cours\" ou \"Manuel, en cours\"" + +#. module: sale +#: code:addons/sale/sale.py:942 +#, python-format +msgid "" +"You have to select a pricelist or a customer in the sales form !\n" +"Please set one before choosing a product." +msgstr "" + +#. module: sale +#: help:sale.order,origin:0 +msgid "Reference of the document that generated this sales order request." +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:955 +#, python-format +msgid "No valid pricelist line found ! :" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:113 +#: code:addons/sale/wizard/sale_make_invoice_advance.py:115 +#, python-format +msgid "Advance of %s %s" +msgstr "" + +#. module: sale +#: view:sale.report:0 +#: field:sale.report,delay:0 +msgid "Commitment Delay" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_quotations +#: model:ir.ui.menu,name:sale.menu_sale_quotations +#: view:sale.order:0 +#: view:sale.report:0 +msgid "Quotations" +msgstr "Soumissions" + +#. module: sale +#: help:account.config.settings,module_sale_analytic_plans:0 +msgid "This allows install module sale_analytic_plans." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Ignore Exception" +msgstr "" + +#. module: sale +#: help:sale.order,partner_shipping_id:0 +msgid "Delivery address for current sales order." +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_sale_margin:0 +msgid "Display margins on sales orders" +msgstr "" + +#. module: sale +#: help:sale.order,invoice_ids:0 +msgid "" +"This is the list of invoices that have been generated for this sales order. " +"The same sales order may have been invoiced in several times (by line for " +"example)." +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Your Reference" +msgstr "" + +#. module: sale +#: view:sale.advance.payment.inv:0 +msgid "Show Lines to Invoice" +msgstr "" + +#. module: sale +#: field:sale.report,date:0 +msgid "Date Order" +msgstr "" + +#. module: sale +#: field:sale.order,pricelist_id:0 +#: field:sale.report,pricelist_id:0 +#: field:sale.shop,pricelist_id:0 +msgid "Pricelist" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "TVA :" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:444 +#, python-format +msgid "Customer Invoices" +msgstr "" + +#. module: sale +#: model:process.node,note:sale.process_node_order0 +msgid "Confirmed sales order to invoice." +msgstr "" + +#. module: sale +#: field:sale.order.line,sequence:0 +msgid "Sequence" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.open_board_sales +#: model:ir.ui.menu,name:sale.menu_dashboard_sales +#: model:process.process,name:sale.process_process_salesprocess0 +#: view:res.partner:0 +#: view:sale.order:0 +#: view:sale.report:0 +msgid "Sales" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:308 +#, python-format +msgid "" +"If you change the pricelist of this order (and eventually the currency), " +"prices of existing order lines will not be updated." +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Qty" +msgstr "" + +#. module: sale +#: selection:sale.order.line,type:0 +msgid "from stock" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Quotation Date" +msgstr "Date de soumission" + +#. module: sale +#: field:sale.order,amount_total:0 +#: view:sale.order.line:0 +msgid "Total" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: selection:sale.order,state:0 +#: view:sale.order.line:0 +#: selection:sale.order.line,state:0 +#: selection:sale.report,state:0 +msgid "Done" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Invoice address :" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_line_invoice.py:121 +#: model:ir.model,name:sale.model_account_invoice +#: model:process.node,name:sale.process_node_invoice0 +#: view:sale.order:0 +#, python-format +msgid "Invoice" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "My Sales Order Lines" +msgstr "" + +#. module: sale +#: model:process.transition.action,name:sale.process_transition_action_cancel0 +#: view:sale.advance.payment.inv:0 +#: view:sale.make.invoice:0 +#: view:sale.order.line.make.invoice:0 +msgid "Cancel" +msgstr "" + +#. module: sale +#: field:sale.order,message_follower_ids:0 +msgid "Followers" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:944 +#, python-format +msgid "No Pricelist ! : " +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Sales Order " +msgstr "" + +#. module: sale +#: model:mail.message.subtype,description:sale.mt_order_sent +#: model:mail.message.subtype,name:sale.mt_order_sent +msgid "Quotation send" +msgstr "Envoi de la soumission" + +#. module: sale +#: view:sale.order.line:0 +msgid "Search Uninvoiced Lines" +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_account_config_settings +msgid "account.config.settings" +msgstr "" + +#. module: sale +#: sql_constraint:sale.order:0 +msgid "Order Reference must be unique per Company!" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,help:sale.action_order_line_tree2 +msgid "" +"

\n" +" Here is a list of each sales order line to be invoiced. You " +"can\n" +" invoice sales orders partially, by lines of sales order. You " +"do\n" +" not need this list if you invoice from the delivery orders " +"or\n" +" if you invoice sales totally.\n" +"

\n" +" " +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Shipping address :" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:505 +#, python-format +msgid "" +"You cannot group sales having different currencies for the same partner." +msgstr "" + +#. module: sale +#: view:sale.advance.payment.inv:0 +msgid "Invoice Sales Order" +msgstr "" + +#. module: sale +#: help:sale.order,invoice_quantity:0 +msgid "" +"The sales order will automatically create the invoice proposition (draft " +"invoice). You have to choose " +"if you want your invoice based on ordered " +msgstr "" + +#. module: sale +#: field:sale.config.settings,module_account_analytic_analysis:0 +msgid "Use contracts management" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:952 +#, python-format +msgid "" +"Cannot find a pricelist line matching this product and quantity.\n" +"You have to change either the product, the quantity or the pricelist." +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_advance_payment_inv +msgid "Sales Advance Payment Invoice" +msgstr "" + +#. module: sale +#: model:process.transition,name:sale.process_transition_invoice0 +#: model:process.transition.action,name:sale.process_transition_action_createinvoice0 +#: view:sale.advance.payment.inv:0 +#: view:sale.order:0 +#: field:sale.order,order_policy:0 +#: view:sale.order.line:0 +msgid "Create Invoice" +msgstr "" + +#. module: sale +#: help:sale.order,amount_untaxed:0 +msgid "The amount without tax." +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Order reference" +msgstr "" + +#. module: sale +#: help:sale.order,invoiced:0 +msgid "It indicates that an invoice has been paid." +msgstr "" + +#. module: sale +#: report:sale.order:0 +#: field:sale.order.line,price_unit:0 +msgid "Unit Price" +msgstr "" + +#. module: sale +#: selection:sale.advance.payment.inv,advance_payment_method:0 +msgid "Percentage" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Other Information" +msgstr "" + +#. module: sale +#: model:email.template,body_html:sale.email_template_edi_sale +msgid "" +"\n" +"
\n" +"\n" +"

Hello ${object.partner_id.name},

\n" +" \n" +"

Here is your ${object.state in ('draft', 'sent') and 'quotation' or " +"'order confirmation'} from ${object.company_id.name}:

\n" +"\n" +"

\n" +"   REFERENCES
\n" +"   Order number: ${object.name}
\n" +"   Order total: ${object.amount_total} " +"${object.pricelist_id.currency_id.name}
\n" +"   Order date: ${object.date_order}
\n" +" % if object.origin:\n" +"   Order reference: ${object.origin}
\n" +" % endif\n" +" % if object.client_order_ref:\n" +"   Your reference: ${object.client_order_ref}
\n" +" % endif\n" +" % if object.user_id:\n" +"   Your contact: ${object.user_id.name}\n" +" % endif\n" +"

\n" +"\n" +" % if object.paypal_url:\n" +"
\n" +"

It is also possible to directly pay with Paypal:

\n" +" \n" +" \n" +" \n" +" % endif\n" +"\n" +"
\n" +"

If you have any question, do not hesitate to contact us.

\n" +"

Thank you for choosing ${object.company_id.name or 'us'}!

\n" +"
\n" +"
\n" +"
\n" +"

\n" +" ${object.company_id.name}

\n" +"
\n" +"
\n" +" \n" +" % if object.company_id.street:\n" +" ${object.company_id.street}
\n" +" % endif\n" +" % if object.company_id.street2:\n" +" ${object.company_id.street2}
\n" +" % endif\n" +" % if object.company_id.city or object.company_id.zip:\n" +" ${object.company_id.zip} ${object.company_id.city}
\n" +" % endif\n" +" % if object.company_id.country_id:\n" +" ${object.company_id.state_id and ('%s, ' % " +"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name " +"or ''}
\n" +" % endif\n" +"
\n" +" % if object.company_id.phone:\n" +"
\n" +" Phone:  ${object.company_id.phone}\n" +"
\n" +" % endif\n" +" % if object.company_id.website:\n" +"
\n" +" Web : ${object.company_id.website}\n" +"
\n" +" %endif\n" +"

\n" +"
\n" +"
\n" +" " +msgstr "" +"\n" +"
\n" +"\n" +"

Bonjour ${object.partner_id.name},

\n" +" \n" +"

Veuillez trouver ci-joint votre ${object.state in ('draft', 'sent') " +"and 'soumission' or 'confirmation de commande'} de la part de " +"${object.company_id.name}:

\n" +"\n" +"

\n" +"   RÉFÉRENCES
\n" +"   Numéro: ${object.name}
\n" +"   Montant total: ${object.amount_total} " +"${object.pricelist_id.currency_id.name}
\n" +"   Date: ${object.date_order}
\n" +" % if object.origin:\n" +"   Référence: ${object.origin}
\n" +" % endif\n" +" % if object.client_order_ref:\n" +"   Votre référence: ${object.client_order_ref}
\n" +" % endif\n" +" % if object.user_id:\n" +"   Votre contact: ${object.user_id.name}\n" +" % endif\n" +"

\n" +"\n" +" % if object.paypal_url:\n" +"
\n" +"

Il vous est possible de payer directement avec Paypal:

\n" +" \n" +" \n" +" \n" +" % endif\n" +"\n" +"
\n" +"

Si vous avez des questions, n'hésitez pas à nous contacter.

\n" +"

Merci d'avoir choisi ${object.company_id.name or 'us'}!

\n" +"
\n" +"
\n" +"
\n" +"

\n" +" ${object.company_id.name}

\n" +"
\n" +"
\n" +" \n" +" % if object.company_id.street:\n" +" ${object.company_id.street}
\n" +" % endif\n" +" % if object.company_id.street2:\n" +" ${object.company_id.street2}
\n" +" % endif\n" +" % if object.company_id.city or object.company_id.zip:\n" +" ${object.company_id.zip} ${object.company_id.city}
\n" +" % endif\n" +" % if object.company_id.country_id:\n" +" ${object.company_id.state_id and ('%s, ' % " +"object.company_id.state_id.name) or ''} ${object.company_id.country_id.name " +"or ''}
\n" +" % endif\n" +"
\n" +" % if object.company_id.phone:\n" +"
\n" +" Tél:  ${object.company_id.phone}\n" +"
\n" +" % endif\n" +" % if object.company_id.website:\n" +"
\n" +" Web : ${object.company_id.website}\n" +"
\n" +" %endif\n" +"

\n" +"
\n" +"
\n" +" " + +#. module: sale +#: view:sale.order.line:0 +#: field:sale.order.line,product_id:0 +#: view:sale.report:0 +#: field:sale.report,product_id:0 +msgid "Product" +msgstr "" + +#. module: sale +#: help:sale.order,amount_total:0 +msgid "The total amount." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Confirm Sale" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_make_invoice_advance.py:96 +#, python-format +msgid "There is no income account defined for this product: \"%s\" (id:%d)." +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "May" +msgstr "" + +#. module: sale +#: model:email.template,subject:sale.email_template_edi_sale +msgid "" +"${object.company_id.name} ${object.state in ('draft', 'sent') and " +"'Quotation' or 'Order'} (Ref ${object.name or 'n/a' })" +msgstr "" +"${object.company_id.name} ${object.state in ('draft', 'sent') and " +"'Soumission' or 'Commande'} (Réf. ${object.name or 'n/a' })" + +#. module: sale +#: report:sale.order:0 +msgid "Price" +msgstr "" + +#. module: sale +#: help:sale.order,order_policy:0 +msgid "" +"On demand: A draft invoice can be created from the sales order when needed. " +"\n" +"On delivery order: A draft invoice can be created from the delivery order " +"when the products have been delivered. \n" +"Before delivery: A draft invoice is created from the sales order and must be " +"paid before the products can be delivered." +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,help:sale.action_order_report_all +msgid "" +"This report performs analysis on your quotations and sales orders. Analysis " +"check your sales revenues and sort it by different group criteria (salesman, " +"partner, product, etc.) Use this report to perform analysis on sales not " +"having invoiced yet. If you want to analyse your turnover, you should use " +"the Invoice Analysis report in the Accounting application." +msgstr "" +"Ce rapport analyse vos soumissions et commandes clients. Il vérifie les " +"revenus liés aux ventes et les classifie selon différents critères (vendeur, " +"partenaire, article, etc.). Vous pouvez utiliser ce rapport pour analyser " +"vos ventes non encore facturées. Si vous souhaitez analyser votre chiffre " +"d'affaires, vous devriez utiliser le rapport \"Analyse des factures " +"clients\" dans l'application comptabilité." + +#. module: sale +#: model:ir.actions.report.xml,name:sale.report_sale_order +msgid "Quotation / Order" +msgstr "Soumission / Commande" + +#. module: sale +#: report:sale.order:0 +msgid "Tel. :" +msgstr "" + +#. module: sale +#: view:sale.make.invoice:0 +msgid "Do you really want to create the invoice(s)?" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,help:sale.action_quotations +msgid "" +"

\n" +" Click to create a quotation, the first step of a new sale.\n" +"

\n" +" OpenERP will help you handle efficiently the complete sale " +"flow:\n" +" from the quotation to the sales order, the\n" +" delivery, the invoicing and the payment collection.\n" +"

\n" +" The social feature helps you organize discussions on each " +"sales\n" +" order, and allow your customers to keep track of the " +"evolution\n" +" of the sales order.\n" +"

\n" +" " +msgstr "" +"

\n" +" Cliquez pour créer une soumission, la première étape d'une " +"commande de vente.\n" +"

\n" +" OpenERP vous aidera à compléter efficacement le flux de " +"vente :\n" +" de la soumission à la commande de vente, la \n" +" livraison, la facturation et les paiements.\n" +"

\n" +" La fonctionnalité sociale vous aide à organiser les " +"discussions sur chaque\n" +" commande de vente, et permet à vos clients de garder une " +"trace de \n" +" l'évolution des commandes de vente.\n" +"

\n" +" " + +#. module: sale +#: view:res.partner:0 +msgid "sale.group_delivery_invoice_address" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:781 +#, python-format +msgid "" +"There is no Fiscal Position defined or Income category account defined for " +"default properties of Product categories." +msgstr "" + +#. module: sale +#: model:process.node,note:sale.process_node_invoice0 +msgid "To be reviewed by the accountant." +msgstr "" + +#. module: sale +#: model:res.groups,name:sale.group_mrp_properties +msgid "Properties on lines" +msgstr "" + +#. module: sale +#: selection:sale.order,state:0 +msgid "Sale to Invoice" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Order Number" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: field:sale.order,partner_id:0 +#: field:sale.order.line,order_partner_id:0 +msgid "Customer" +msgstr "" + +#. module: sale +#: model:product.template,name:sale.advance_product_0_product_template +msgid "Advance" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "February" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,help:sale.action_orders +msgid "" +"

\n" +" Click to create a quotation that can be converted into a " +"sales\n" +" order.\n" +"

\n" +" OpenERP will help you efficiently handle the complete sales " +"flow:\n" +" quotation, sales order, delivery, invoicing and payment.\n" +"

\n" +" " +msgstr "" +"

\n" +" Cliquez pour créer une soumission qui pourra être convertie " +"en commande\n" +" de vente.\n" +"

\n" +" OpenERP vous aidera à compléter efficacement le flux de " +"vente :\n" +" soumission, commande, livraison, facturation et paiement.\n" +"

\n" +" " + +#. module: sale +#: field:sale.order,invoice_quantity:0 +msgid "Invoice on" +msgstr "" + +#. module: sale +#: selection:sale.advance.payment.inv,advance_payment_method:0 +msgid "Fixed price (deposit)" +msgstr "" + +#. module: sale +#: model:ir.model,name:sale.model_sale_config_settings +msgid "sale.config.settings" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "September" +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Date Ordered" +msgstr "" + +#. module: sale +#: selection:sale.order,order_policy:0 +msgid "Before Delivery" +msgstr "" + +#. module: sale +#: field:sale.order.line,product_uos:0 +msgid "Product UoS" +msgstr "" + +#. module: sale +#: model:process.node,note:sale.process_node_quotation0 +msgid "Draft state of sales order" +msgstr "" + +#. module: sale +#: field:sale.order,origin:0 +msgid "Source Document" +msgstr "" + +#. module: sale +#: selection:sale.report,month:0 +msgid "April" +msgstr "" + +#. module: sale +#: selection:sale.report,state:0 +msgid "Manual In Progress" +msgstr "" + +#. module: sale +#: code:addons/sale/wizard/sale_line_invoice.py:107 +#: code:addons/sale/wizard/sale_make_invoice.py:42 +#: code:addons/sale/wizard/sale_make_invoice.py:55 +#, python-format +msgid "Warning!" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_order_tree +msgid "Old Quotations" +msgstr "Anciennes soumissions" + +#. module: sale +#: model:ir.actions.act_window,help:sale.act_res_partner_2_sale_order +msgid "" +"

\n" +" Click to create a quotation or sales order for this " +"customer.\n" +"

\n" +" OpenERP will help you efficiently handle the complete sale " +"flow:\n" +" quotation, sales order, delivery, invoicing and\n" +" payment.\n" +"

\n" +" The social feature helps you organize discussions on each " +"sales\n" +" order, and allow your customer to keep track of the " +"evolution\n" +" of the sales order.\n" +"

\n" +" " +msgstr "" +"

\n" +" Cliquez pour créer une soumission ou une commande de vente " +"pour votre client.\n" +"

\n" +" OpenERP vous aidera à compléter efficacement le flux de " +"vente :\n" +" soumission, commande, livraison, facturation et paiement.\n" +"

\n" +" La fonctionnalité sociale vous aide à organiser les " +"discussions sur chaque\n" +" commande de vente, et permet à vos clients de garder une " +"trace de \n" +" l'évolution des commandes de vente.\n" +"\n" +"

\n" +" " + +#. module: sale +#: field:sale.order,message_summary:0 +msgid "Summary" +msgstr "" + +#. module: sale +#: help:sale.order,message_ids:0 +msgid "Messages and communication history" +msgstr "" + +#. module: sale +#: view:sale.order:0 +#: view:sale.order.line:0 +msgid "Search Sales Order" +msgstr "" + +#. module: sale +#: report:sale.order:0 +#: field:sale.order.line,name:0 +msgid "Description" +msgstr "" + +#. module: sale +#: view:sale.config.settings:0 +msgid "" +"Use contract to be able to manage your services with\n" +" multiple invoicing as part of the same contract " +"with\n" +" your customer." +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "Ordered month of the sales order" +msgstr "" + +#. module: sale +#: model:process.transition,name:sale.process_transition_saleinvoice0 +msgid "From a sales order" +msgstr "" + +#. module: sale +#: view:sale.order.line:0 +msgid "Confirmed sales order lines, not yet delivered" +msgstr "" + +#. module: sale +#: model:process.transition,note:sale.process_transition_saleinvoice0 +msgid "" +"Depending on the Invoicing control of the sales order, the invoice can be " +"based on delivered or on ordered quantities. Thus, a sales order can " +"generates an invoice or a delivery order as soon as it is confirmed by the " +"salesman." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "UoS" +msgstr "" + +#. module: sale +#: selection:sale.advance.payment.inv,advance_payment_method:0 +msgid "Some order lines" +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:983 +#, python-format +msgid "Cannot delete a sales order line which is in state '%s'." +msgstr "" + +#. module: sale +#: report:sale.order:0 +#: field:sale.order,payment_term:0 +msgid "Payment Term" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Sales Order ready to be invoiced" +msgstr "" + +#. module: sale +#: field:sale.config.settings,group_invoice_so_lines:0 +msgid "Generate invoices based on the sales order lines" +msgstr "" + +#. module: sale +#: view:sale.advance.payment.inv:0 +#: view:sale.make.invoice:0 +#: view:sale.order.line.make.invoice:0 +msgid "or" +msgstr "" + +#. module: sale +#: model:ir.actions.act_window,name:sale.action_order_line_product_tree +#: view:sale.order:0 +#: view:sale.order.line:0 +msgid "Sales Order Lines" +msgstr "" + +#. module: sale +#: help:sale.order,pricelist_id:0 +msgid "Pricelist for current sales order." +msgstr "" + +#. module: sale +#: report:sale.order:0 +msgid "Quotation N°" +msgstr "Soumission N°" + +#. module: sale +#: model:res.groups,name:sale.group_discount_per_so_line +msgid "Discount on lines" +msgstr "" + +#. module: sale +#: field:sale.order,client_order_ref:0 +msgid "Customer Reference" +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "Picked" +msgstr "" + +#. module: sale +#: help:sale.config.settings,module_sale_margin:0 +msgid "" +"This adds the 'Margin' on sales order.\n" +" This gives the profitability by calculating the difference " +"between the Unit Price and Cost Price.\n" +" This installs the module sale_margin." +msgstr "" + +#. module: sale +#: code:addons/sale/sale.py:865 +#, python-format +msgid "" +"Before choosing a product,\n" +" select a customer in the sales form." +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "Total Tax Included" +msgstr "" + +#. module: sale +#: view:sale.order:0 +msgid "New Copy of Quotation" +msgstr "Nouvelle copie de soumission" + +#. module: sale +#: view:sale.advance.payment.inv:0 +msgid "" +"Select how you want to invoice this order. This\n" +" will create a draft invoice that can be modified\n" +" before validation." +msgstr "" + +#. module: sale +#: view:sale.report:0 +msgid "Ordered date of the sales order" +msgstr "" diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 358f105b0d0..b25509180b0 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -46,10 +46,12 @@ class ir_http(orm.AbstractModel): try: func, arguments = self._find_handler() request.website_enabled = func.routing.get('website', False) + request.website_multilang = func.routing.get('multilang', False) except werkzeug.exceptions.NotFound: # either we have a language prefixed route, either a real 404 # in all cases, website processes them request.website_enabled = True + request.website_multilang = True if request.website_enabled: if func: diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 890d6080039..751a37b3455 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -16,8 +16,6 @@ - - @@ -83,10 +81,12 @@ and main_object.website_meta_keywords or website_meta_keywords"/> <t t-esc="title"/> - - - - + + + + + + @@ -177,7 +177,7 @@ their performance.

-