diff --git a/addons/account/account.py b/addons/account/account.py index aa5d507b3d2..51700d30731 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -2055,6 +2055,8 @@ class account_tax(osv.osv): amount = amount2 child_tax = self._unit_compute(cr, uid, tax.child_ids, amount, product, partner, quantity) res.extend(child_tax) + for child in child_tax: + amount2 += child.get('amount', 0.0) if tax.child_depend: for r in res: for name in ('base','ref_base'): diff --git a/addons/account/edi/invoice.py b/addons/account/edi/invoice.py index 2c2a754de3b..c82d041ef85 100644 --- a/addons/account/edi/invoice.py +++ b/addons/account/edi/invoice.py @@ -21,7 +21,7 @@ from openerp.osv import osv, fields from openerp.addons.edi import EDIMixin -from urllib import urlencode +from werkzeug import url_encode INVOICE_LINE_EDI_STRUCT = { 'name': True, @@ -274,7 +274,7 @@ class account_invoice(osv.osv, EDIMixin): "no_note": "1", "bn": "OpenERP_Invoice_PayNow_" + inv.currency_id.name, } - res[inv.id] = "https://www.paypal.com/cgi-bin/webscr?" + urlencode(params) + res[inv.id] = "https://www.paypal.com/cgi-bin/webscr?" + url_encode(params) return res _columns = { diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index c43e978e590..8d470f45637 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -62,22 +62,28 @@ class account_analytic_invoice_line(osv.osv): context = context or {} uom_obj = self.pool.get('product.uom') company_id = company_id or False - context.update({'company_id': company_id, 'force_company': company_id, 'pricelist_id': pricelist_id}) + local_context = dict(context, company_id=company_id, force_company=company_id, pricelist=pricelist_id) if not product: return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}} if partner_id: - part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) + part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=local_context) if part.lang: context.update({'lang': part.lang}) result = {} - res = self.pool.get('product.product').browse(cr, uid, product, context=context) - result.update({'name': name or res.description or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': price_unit or res.list_price or 0.0}) + res = self.pool.get('product.product').browse(cr, uid, product, context=local_context) + if price_unit is not False: + price = price_unit + elif pricelist_id: + price = res.price + else: + price = res.list_price + result.update({'name': name or res.description or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': price}) res_final = {'value':result} if result['uom_id'] != res.uom_id.id: - selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=context) + selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=local_context) new_price = uom_obj._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uom_id']) res_final['value']['price_unit'] = new_price return res_final 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/analytic/analytic.py b/addons/analytic/analytic.py index c1ea1f655d2..61e5c1166ea 100644 --- a/addons/analytic/analytic.py +++ b/addons/analytic/analytic.py @@ -220,7 +220,7 @@ class account_analytic_account(osv.osv): res['value']['description'] = template.description return res - def on_change_partner_id(self, cr, uid, ids,partner_id, name, context={}): + def on_change_partner_id(self, cr, uid, ids,partner_id, name, context=None): res={} if partner_id: partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) diff --git a/addons/auth_oauth/auth_oauth_data.xml b/addons/auth_oauth/auth_oauth_data.xml index 6abdab41b7c..fe64f42b2f6 100644 --- a/addons/auth_oauth/auth_oauth_data.xml +++ b/addons/auth_oauth/auth_oauth_data.xml @@ -2,13 +2,13 @@ - OpenERP.com Accounts - https://accounts.openerp.com/oauth2/auth + Odoo.com Accounts + https://accounts.odoo.com/oauth2/auth userinfo - https://accounts.openerp.com/oauth2/tokeninfo + https://accounts.odoo.com/oauth2/tokeninfo zocial openerp - Log in with OpenERP.com + Log in with Odoo.com diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 8a66d3ab075..3040fafaf2b 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -42,7 +42,7 @@ class AuthSignupHome(openerp.addons.web.controllers.main.Home): return http.redirect_with_hash(request.params.get('redirect')) return response - @http.route('/web/signup', type='http', auth='public', website=True, multilang=True) + @http.route('/web/signup', type='http', auth='public', website=True) def web_auth_signup(self, *args, **kw): qcontext = self.get_auth_signup_qcontext() @@ -58,7 +58,7 @@ class AuthSignupHome(openerp.addons.web.controllers.main.Home): return request.render('auth_signup.signup', qcontext) - @http.route('/web/reset_password', type='http', auth='public', website=True, multilang=True) + @http.route('/web/reset_password', type='http', auth='public', website=True) def web_auth_reset_password(self, *args, **kw): qcontext = self.get_auth_signup_qcontext() diff --git a/addons/auth_signup/i18n/de.po b/addons/auth_signup/i18n/de.po index 756a19cdf8f..c6e8f777d80 100644 --- a/addons/auth_signup/i18n/de.po +++ b/addons/auth_signup/i18n/de.po @@ -8,14 +8,14 @@ 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: 2012-12-19 14:58+0000\n" -"Last-Translator: Ferdinand \n" +"PO-Revision-Date: 2014-05-14 11:25+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-04-22 07:50+0000\n" -"X-Generator: Launchpad (build 16985)\n" +"X-Launchpad-Export-Date: 2014-05-15 05:51+0000\n" +"X-Generator: Launchpad (build 17007)\n" #. module: auth_signup #: field:res.partner,signup_type:0 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/crm/__init__.py b/addons/crm/__init__.py index e5c06c53d97..872d0390303 100644 --- a/addons/crm/__init__.py +++ b/addons/crm/__init__.py @@ -22,6 +22,7 @@ import crm import crm_segmentation import crm_lead +import sales_team import calendar_event import crm_phonecall import report diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index d53f785d174..15544dc3dff 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -51,13 +51,13 @@ Dashboard for CRM will include: 'depends': [ 'base_action_rule', 'base_setup', + 'sales_team', 'mail', 'email_template', 'calendar', 'resource', 'board', 'fetchmail', - 'web_kanban_sparkline', ], 'data': [ 'crm_data.xml', @@ -91,8 +91,7 @@ Dashboard for CRM will include: 'res_config_view.xml', 'base_partner_merge_view.xml', - 'crm_case_section_view.xml', - 'views/crm.xml', + 'sales_team_view.xml', ], 'demo': [ 'crm_demo.xml', diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 29aa900c16d..b114a6e24fe 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -88,154 +88,6 @@ class crm_case_stage(osv.osv): } -class crm_case_section(osv.osv): - """ Model for sales teams. """ - _name = "crm.case.section" - _inherits = {'mail.alias': 'alias_id'} - _inherit = "mail.thread" - _description = "Sales Teams" - _order = "complete_name" - # number of periods for lead/opportunities/... tracking in salesteam kanban dashboard/kanban view - _period_number = 5 - - def get_full_name(self, cr, uid, ids, field_name, arg, context=None): - return dict(self.name_get(cr, uid, ids, context=context)) - - def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None): - """ Generic method to generate data for bar chart values using SparklineBarWidget. - This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field). - - :param obj: the target model (i.e. crm_lead) - :param domain: the domain applied to the read_group - :param list read_fields: the list of fields to read in the read_group - :param str value_field: the field used to compute the value of the bar slice - :param str groupby_field: the fields used to group - - :return list section_result: a list of dicts: [ - { 'value': (int) bar_column_value, - 'tootip': (str) bar_column_tooltip, - } - ] - """ - month_begin = date.today().replace(day=1) - section_result = [{ - 'value': 0, - 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B %Y'), - } for i in range(self._period_number - 1, -1, -1)] - group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context) - pattern = tools.DEFAULT_SERVER_DATE_FORMAT if obj.fields_get(cr, uid, groupby_field)[groupby_field]['type'] == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT - for group in group_obj: - group_begin_date = datetime.strptime(group['__domain'][0][2], pattern) - month_delta = relativedelta.relativedelta(month_begin, group_begin_date) - section_result[self._period_number - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field, 0)} - return section_result - - def _get_opportunities_data(self, cr, uid, ids, field_name, arg, context=None): - """ Get opportunities-related data for salesteam kanban view - monthly_open_leads: number of open lead during the last months - monthly_planned_revenue: planned revenu of opportunities during the last months - """ - obj = self.pool.get('crm.lead') - res = dict.fromkeys(ids, False) - month_begin = date.today().replace(day=1) - date_begin = month_begin - relativedelta.relativedelta(months=self._period_number - 1) - date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]) - lead_pre_domain = [('create_date', '>=', date_begin.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)), - ('create_date', '<=', date_end.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)), - ('type', '=', 'lead')] - opp_pre_domain = [('date_deadline', '>=', date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)), - ('date_deadline', '<=', date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)), - ('type', '=', 'opportunity')] - for id in ids: - res[id] = dict() - lead_domain = lead_pre_domain + [('section_id', '=', id)] - opp_domain = opp_pre_domain + [('section_id', '=', id)] - res[id]['monthly_open_leads'] = self.__get_bar_values(cr, uid, obj, lead_domain, ['create_date'], 'create_date_count', 'create_date', context=context) - res[id]['monthly_planned_revenue'] = self.__get_bar_values(cr, uid, obj, opp_domain, ['planned_revenue', 'date_deadline'], 'planned_revenue', 'date_deadline', context=context) - return res - - _columns = { - 'name': fields.char('Sales Team', size=64, required=True, translate=True), - 'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True), - 'code': fields.char('Code', size=8), - 'active': fields.boolean('Active', help="If the active field is set to "\ - "true, it will allow you to hide the sales team without removing it."), - 'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the salesman with the team leader."), - 'user_id': fields.many2one('res.users', 'Team Leader'), - 'member_ids': fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'), - 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this sales team"), - 'parent_id': fields.many2one('crm.case.section', 'Parent Team'), - 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'), - 'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"), - 'note': fields.text('Description'), - 'working_hours': fields.float('Working Hours', digits=(16, 2)), - 'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'), - 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True, - help="The email address associated with this team. New emails received will automatically " - "create new leads assigned to the team."), - 'color': fields.integer('Color Index'), - 'use_leads': fields.boolean('Leads', - help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team."), - - 'monthly_open_leads': fields.function(_get_opportunities_data, - type="string", readonly=True, multi='_get_opportunities_data', - string='Open Leads per Month'), - 'monthly_planned_revenue': fields.function(_get_opportunities_data, - type="string", readonly=True, multi='_get_opportunities_data', - string='Planned Revenue per Month') - } - - def _get_stage_common(self, cr, uid, context): - ids = self.pool.get('crm.case.stage').search(cr, uid, [('case_default', '=', 1)], context=context) - return ids - - _defaults = { - 'active': 1, - 'stage_ids': _get_stage_common, - 'use_leads': True, - } - - _sql_constraints = [ - ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !') - ] - - _constraints = [ - (osv.osv._check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id']) - ] - - def name_get(self, cr, uid, ids, context=None): - """Overrides orm name_get method""" - if not isinstance(ids, list): - ids = [ids] - res = [] - if not ids: - return res - reads = self.read(cr, uid, ids, ['name', 'parent_id'], context) - - for record in reads: - name = record['name'] - if record['parent_id']: - name = record['parent_id'][1] + ' / ' + name - res.append((record['id'], name)) - return res - - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - create_context = dict(context, alias_model_name='crm.lead', alias_parent_model_name=self._name) - section_id = super(crm_case_section, self).create(cr, uid, vals, context=create_context) - section = self.browse(cr, uid, section_id, context=context) - self.pool.get('mail.alias').write(cr, uid, [section.alias_id.id], {'alias_parent_thread_id': section_id, 'alias_defaults': {'section_id': section_id, 'type': 'lead'}}, context=context) - return section_id - - def unlink(self, cr, uid, ids, context=None): - # Cascade-delete mail aliases as well, as they should not exist without the sales team. - mail_alias = self.pool.get('mail.alias') - alias_ids = [team.alias_id.id for team in self.browse(cr, uid, ids, context=context) if team.alias_id] - res = super(crm_case_section, self).unlink(cr, uid, ids, context=context) - mail_alias.unlink(cr, uid, alias_ids, context=context) - return res - class crm_case_categ(osv.osv): """ Category of Case """ _name = "crm.case.categ" diff --git a/addons/crm/crm_action_rule_demo.xml b/addons/crm/crm_action_rule_demo.xml index 8eac91c06e2..d77211a0f12 100644 --- a/addons/crm/crm_action_rule_demo.xml +++ b/addons/crm/crm_action_rule_demo.xml @@ -41,7 +41,7 @@ True ir.actions.server code - sales_team = self.pool.get('ir.model.data').get_object(cr, uid, 'crm', 'section_sales_department') + sales_team = self.pool.get('ir.model.data').get_object(cr, uid, 'sales_team', 'section_sales_department') object.write({'section_id': sales_team.id}) diff --git a/addons/crm/crm_case_section_view.xml b/addons/crm/crm_case_section_view.xml deleted file mode 100644 index 04ad124f652..00000000000 --- a/addons/crm/crm_case_section_view.xml +++ /dev/null @@ -1,331 +0,0 @@ - - - - - - - - Leads - crm.lead - tree,form - ['|', ('type','=','lead'), ('type','=',False)] - - - { - 'search_default_section_id': [active_id], - 'default_section_id': active_id, - 'default_type': 'lead', - 'stage_type': 'lead', - } - - -

- Use leads if you need a qualification step before creating an - opportunity or a customer. It can be a business card you received, - a contact form filled in your website, or a file of unqualified - prospects you import, etc. -

- Once qualified, the lead can be converted into a business - opportunity and/or a new customer in your address book. -

-
-
- - - - - Opportunities - crm.lead - kanban,tree,graph,form,calendar - [('type','=','opportunity')] - - - { - 'search_default_section_id': [active_id], - 'default_section_id': active_id, - 'stage_type': 'opportunity', - 'default_type': 'opportunity', - 'default_user_id': uid, - } - - -

- OpenERP helps you keep track of your sales pipeline to follow - up potential sales and better forecast your future revenues. -

- You will be able to plan meetings and phone calls from - opportunities, convert them into quotations, attach related - documents, track all discussions, and much more. -

-
-
- - - Leads Analysis - crm.lead.report - {"search_default_month":1} - graph - - [('type','=', 'lead'),('section_id', '=', active_id)] - Leads Analysis allows you to check different CRM related information like the treatment delays or number of leads per state. You can sort out your leads analysis by different groups to get accurate grained analysis. - - - - Opportunities Analysis - crm.lead.report - graph - - [('type','=', 'opportunity'), ('section_id', '=', active_id)] - Opportunities Analysis gives you an instant access to your opportunities with information such as the expected revenue, planned cost, missed deadlines or the number of interactions per opportunity. This report is mainly used by the sales manager in order to do the periodic review with the teams of the sales pipeline. - - - - - - crm.case.section.kanban - crm.case.section - - - - - - - - - - - - - - - - - - - - - - - - Case Sections - Search - crm.case.section - - - - - - - - - - - - - - - - - - - - Sales Teams - crm.case.section - form - kanban,tree,form - {'search_default_personal': True} - - -

- Click here to define a new sales team. -

- Use sales team to organize your different salespersons or - departments into separate teams. Each team will work in - its own list of opportunities. -

-
-
- - - - - crm.case.section.form - crm.case.section - -
- -
-
- - - - - - - - - - - - - - - - - - -
- X -
-
- -
-
-
-
-
-
-
-
- - - - - - - -
-
-
- - -
-
-
-
- - - - - crm.case.section.tree - crm.case.section - child_ids - - - - - - - - - - - - - - kanban - - - - - - - tree - - - - - - - form - - - - - - Cases by Sales Team - crm.case.section - [('parent_id','=',False)] - tree - - - - - Sales Teams - crm.case.section - form - - -

- Click here to define a new sales team. -

- Use sales team to organize your different salespersons or - departments into separate teams. Each team will work in - its own list of opportunities. -

-
-
- - -
-
\ No newline at end of file diff --git a/addons/crm/crm_data.xml b/addons/crm/crm_data.xml index 231a7124824..a5a732747ab 100644 --- a/addons/crm/crm_data.xml +++ b/addons/crm/crm_data.xml @@ -26,34 +26,31 @@ Email - - Direct Sales - DM + True sales - Cash - + Check - + Credit Card - + Demand Draft - + diff --git a/addons/crm/crm_demo.xml b/addons/crm/crm_demo.xml index 82bdcd8fcd9..df776f92350 100644 --- a/addons/crm/crm_demo.xml +++ b/addons/crm/crm_demo.xml @@ -2,26 +2,6 @@ - - - - - - - - - - Indirect Sales - IM - - - - - Marketing - SPD - - - OpenERP partners True diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index cfec1a3d931..a209bba4890 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -90,7 +90,10 @@ class crm_lead(format_address, osv.osv): def _get_default_section_id(self, cr, uid, context=None): """ Gives default section by checking if present in the context """ - return self._resolve_section_id_from_context(cr, uid, context=context) or False + section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False + if not section_id: + section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False + return section_id def _get_default_stage_id(self, cr, uid, context=None): """ Gives default stage_id """ @@ -167,9 +170,6 @@ class crm_lead(format_address, osv.osv): """ :return dict: difference between current date and log date """ - cal_obj = self.pool.get('resource.calendar') - res_obj = self.pool.get('resource.resource') - res = {} for lead in self.browse(cr, uid, ids, context=context): for field in fields: @@ -181,39 +181,14 @@ class crm_lead(format_address, osv.osv): date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S") date_open = datetime.strptime(lead.date_open, "%Y-%m-%d %H:%M:%S") ans = date_open - date_create - date_until = lead.date_open elif field == 'day_close': if lead.date_closed: date_create = datetime.strptime(lead.create_date, "%Y-%m-%d %H:%M:%S") date_close = datetime.strptime(lead.date_closed, "%Y-%m-%d %H:%M:%S") - date_until = lead.date_closed ans = date_close - date_create if ans: - resource_id = False - if lead.user_id: - resource_ids = res_obj.search(cr, uid, [('user_id','=',lead.user_id.id)]) - if len(resource_ids): - resource_id = resource_ids[0] - - duration = float(ans.days) - if lead.section_id and lead.section_id.resource_calendar_id: - duration = float(ans.days) * 24 - new_dates = cal_obj.interval_get(cr, - uid, - lead.section_id.resource_calendar_id and lead.section_id.resource_calendar_id.id or False, - datetime.strptime(lead.create_date, '%Y-%m-%d %H:%M:%S'), - duration, - resource=resource_id - ) - no_days = [] - date_until = datetime.strptime(date_until, '%Y-%m-%d %H:%M:%S') - for in_time, out_time in new_dates: - if in_time.date not in no_days: - no_days.append(in_time.date) - if out_time > date_until: - break - duration = len(no_days) - res[lead.id][field] = abs(int(duration)) + duration = abs(int(ans.days)) + res[lead.id][field] = duration return res def _meeting_count(self, cr, uid, ids, field_name, arg, context=None): Event = self.pool['calendar.event'] @@ -258,7 +233,7 @@ class crm_lead(format_address, osv.osv): 'day_open': fields.function(_compute_day, string='Days to Open', \ multi='day_open', type="float", store=True), 'day_close': fields.function(_compute_day, string='Days to Close', \ - multi='day_close', type="float", store=True), + multi='day_open', type="float", store=True), 'date_last_stage_update': fields.datetime('Last Stage Update', select=True), # Messaging and marketing @@ -969,7 +944,7 @@ class crm_lead(format_address, osv.osv): if obj.type == 'opportunity': model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor') else: - view_id = super(crm_lead, self).get_formview_id(cr, uid, id, model='crm.lead', context=context) + view_id = super(crm_lead, self).get_formview_id(cr, uid, id, context=context) return view_id def message_get_suggested_recipients(self, cr, uid, ids, context=None): diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml index fef80bb8a53..9ea859937a2 100644 --- a/addons/crm/crm_lead_data.xml +++ b/addons/crm/crm_lead_data.xml @@ -61,7 +61,7 @@ opportunity - + Telesales - + Email Campaign - Services - + Email Campaign - Products - + Twitter Ads - + Google Adwords - + Banner Ads - + Television - + Newsletter - + Product - + Software - + Services - + Information - + Design - + Training - + Consulting - + Other - + diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml index 50a5a369850..8acefd5c610 100644 --- a/addons/crm/crm_lead_demo.xml +++ b/addons/crm/crm_lead_demo.xml @@ -20,7 +20,7 @@ 1 - + Hello, @@ -55,7 +55,7 @@ Could you please send me the details ? 1 - + @@ -84,7 +84,7 @@ Could you please send me the details ? 2 - + @@ -105,7 +105,7 @@ Could you please send me the details ? 2 - + @@ -130,7 +130,7 @@ Could you please send me the details ? 2 - + Hi, @@ -157,7 +157,7 @@ Contact: +1 813 494 5005 2 - + @@ -175,7 +175,7 @@ Contact: +1 813 494 5005 0 - + @@ -194,7 +194,7 @@ Contact: +1 813 494 5005 1 - + @@ -213,7 +213,7 @@ Contact: +1 813 494 5005 2 - + @@ -231,7 +231,7 @@ Contact: +1 813 494 5005 2 - + Hi, @@ -254,7 +254,7 @@ Andrew 2 - + @@ -272,7 +272,7 @@ Andrew 2 - + @@ -310,7 +310,7 @@ Andrew Meeting for pricing information. - + @@ -335,7 +335,7 @@ Andrew Send Catalogue by Email - + @@ -356,7 +356,7 @@ Andrew Call to ask system requirement - + @@ -383,7 +383,7 @@ Andrew Convert to quote - + @@ -408,7 +408,7 @@ Andrew Send price list regarding our interventions - + @@ -435,7 +435,7 @@ Andrew Call to define real needs about training - + @@ -462,7 +462,7 @@ Andrew Ask for the good receprion of the proposition - + @@ -479,7 +479,7 @@ Andrew 2 - + @@ -493,7 +493,7 @@ Andrew 2 - + @@ -511,7 +511,7 @@ Andrew 1 - + @@ -526,7 +526,7 @@ Andrew 0 - + @@ -545,7 +545,7 @@ Andrew 0 - + @@ -568,7 +568,7 @@ Andrew 2 Conf call with technical service - + @@ -592,7 +592,7 @@ Andrew Send Catalogue by Email - + diff --git a/addons/crm/crm_lead_menu.xml b/addons/crm/crm_lead_menu.xml index dffe03dd4b3..60dc307cc0f 100644 --- a/addons/crm/crm_lead_menu.xml +++ b/addons/crm/crm_lead_menu.xml @@ -81,7 +81,7 @@ action="crm_case_category_act_leads_all"/> + groups="base.group_sale_salesman,base.group_sale_manager"/>
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 2441e4197c4..995a1ddea43 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -1,11 +1,9 @@ - - Stage - Search @@ -570,12 +568,12 @@ help="Opportunities that are assigned to me"/> + help="Opportunities that are assigned to any sales teams I am member of" groups="base.group_multi_salesteams"/> - + diff --git a/addons/crm/crm_phonecall_data.xml b/addons/crm/crm_phonecall_data.xml index 0e2bb6511c5..ce82a9a7b51 100644 --- a/addons/crm/crm_phonecall_data.xml +++ b/addons/crm/crm_phonecall_data.xml @@ -6,13 +6,13 @@ --> Inbound - + Outbound - + diff --git a/addons/crm/crm_phonecall_demo.xml b/addons/crm/crm_phonecall_demo.xml index 90cda34cc01..8563a38b075 100644 --- a/addons/crm/crm_phonecall_demo.xml +++ b/addons/crm/crm_phonecall_demo.xml @@ -11,7 +11,7 @@ Left the message done +34 934 340 230 - + @@ -24,7 +24,7 @@ Need more information on the proposed deal done +44 121 690 4596 - + @@ -37,7 +37,7 @@ Ask for convenient time of meeting open +1 786 525 0724 - + @@ -50,7 +50,7 @@ done (077) 582-4035 (077) 341-3591 - + @@ -63,7 +63,7 @@ More information on the proposed deal pending +1 312 349 2324 - + @@ -74,7 +74,7 @@ Proposal for discount offer open +34 230 953 485 - + diff --git a/addons/crm/report/crm_lead_report_view.xml b/addons/crm/report/crm_lead_report_view.xml index 1dda471c2f2..239579a70c2 100644 --- a/addons/crm/report/crm_lead_report_view.xml +++ b/addons/crm/report/crm_lead_report_view.xml @@ -81,7 +81,7 @@ - + diff --git a/addons/crm/report/crm_phonecall_report_view.xml b/addons/crm/report/crm_phonecall_report_view.xml index 32ae9704064..98f4a58d7c4 100644 --- a/addons/crm/report/crm_phonecall_report_view.xml +++ b/addons/crm/report/crm_phonecall_report_view.xml @@ -47,7 +47,7 @@ - + diff --git a/addons/crm/res_config.py b/addons/crm/res_config.py index 1cd31cd0dd0..d48e66e286c 100644 --- a/addons/crm/res_config.py +++ b/addons/crm/res_config.py @@ -27,33 +27,6 @@ class crm_configuration(osv.TransientModel): _name = 'sale.config.settings' _inherit = ['sale.config.settings', 'fetchmail.config.settings'] - def set_group_multi_salesteams(self, cr, uid, ids, context=None): - """ This method is automatically called by res_config as it begins - with set. It is used to implement the 'one group or another' - behavior. We have to perform some group manipulation by hand - because in res_config.execute(), set_* methods are called - after group_*; therefore writing on an hidden res_config file - could not work. - If group_multi_salesteams is checked: remove group_mono_salesteams - from group_user, remove the users. Otherwise, just add - group_mono_salesteams in group_user. - The inverse logic about group_multi_salesteams is managed by the - normal behavior of 'group_multi_salesteams' field. - """ - def ref(xml_id): - mod, xml = xml_id.split('.', 1) - return self.pool['ir.model.data'].get_object(cr, uid, mod, xml, context) - - for obj in self.browse(cr, uid, ids, context=context): - config_group = ref('base.group_mono_salesteams') - base_group = ref('base.group_user') - if obj.group_multi_salesteams: - base_group.write({'implied_ids': [(3, config_group.id)]}) - config_group.write({'users': [(3, u.id) for u in base_group.users]}) - else: - base_group.write({'implied_ids': [(4, config_group.id)]}) - return True - _columns = { 'group_fund_raising': fields.boolean("Manage Fund Raising", implied_group='crm.group_fund_raising', @@ -64,9 +37,6 @@ class crm_configuration(osv.TransientModel): 'module_crm_helpdesk': fields.boolean("Manage Helpdesk and Support", help='Allows you to communicate with Customer, process Customer query, and provide better help and support.\n' '-This installs the module crm_helpdesk.'), - 'group_multi_salesteams': fields.boolean("Organize Sales activities into multiple Sales Teams", - implied_group='base.group_multi_salesteams', - help="""Allows you to use Sales Teams to manage your leads and opportunities."""), 'alias_prefix': fields.char('Default Alias Name for Leads'), 'alias_domain' : fields.char('Alias Domain'), 'group_scheduled_calls': fields.boolean("Schedule calls to manage call center", diff --git a/addons/crm/res_config_view.xml b/addons/crm/res_config_view.xml index ce3c8242f99..cc7bb6b2e4a 100644 --- a/addons/crm/res_config_view.xml +++ b/addons/crm/res_config_view.xml @@ -32,17 +32,6 @@ - - - diff --git a/addons/crm/sales_team.py b/addons/crm/sales_team.py new file mode 100644 index 00000000000..8a1e9f938af --- /dev/null +++ b/addons/crm/sales_team.py @@ -0,0 +1,82 @@ +import calendar +from datetime import date +from dateutil import relativedelta + +from openerp import tools +from openerp.osv import fields, osv + + +class crm_case_section(osv.Model): + _inherit = 'crm.case.section' + _inherits = {'mail.alias': 'alias_id'} + + def _get_opportunities_data(self, cr, uid, ids, field_name, arg, context=None): + """ Get opportunities-related data for salesteam kanban view + monthly_open_leads: number of open lead during the last months + monthly_planned_revenue: planned revenu of opportunities during the last months + """ + obj = self.pool.get('crm.lead') + res = dict.fromkeys(ids, False) + month_begin = date.today().replace(day=1) + date_begin = month_begin - relativedelta.relativedelta(months=self._period_number - 1) + date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]) + lead_pre_domain = [('create_date', '>=', date_begin.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)), + ('create_date', '<=', date_end.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)), + ('type', '=', 'lead')] + opp_pre_domain = [('date_deadline', '>=', date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)), + ('date_deadline', '<=', date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)), + ('type', '=', 'opportunity')] + for id in ids: + res[id] = dict() + lead_domain = lead_pre_domain + [('section_id', '=', id)] + opp_domain = opp_pre_domain + [('section_id', '=', id)] + res[id]['monthly_open_leads'] = self.__get_bar_values(cr, uid, obj, lead_domain, ['create_date'], 'create_date_count', 'create_date', context=context) + res[id]['monthly_planned_revenue'] = self.__get_bar_values(cr, uid, obj, opp_domain, ['planned_revenue', 'date_deadline'], 'planned_revenue', 'date_deadline', context=context) + return res + + _columns = { + 'resource_calendar_id': fields.many2one('resource.calendar', "Working Time", help="Used to compute open days"), + 'stage_ids': fields.many2many('crm.case.stage', 'section_stage_rel', 'section_id', 'stage_id', 'Stages'), + 'use_leads': fields.boolean('Leads', + help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team."), + 'use_opportunities': fields.boolean('Opportunities', help="Check this box to manage opportunities in this sales team."), + 'monthly_open_leads': fields.function(_get_opportunities_data, + type="string", readonly=True, multi='_get_opportunities_data', + string='Open Leads per Month'), + 'monthly_planned_revenue': fields.function(_get_opportunities_data, + type="string", readonly=True, multi='_get_opportunities_data', + string='Planned Revenue per Month'), + 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True, help="The email address associated with this team. New emails received will automatically create new leads assigned to the team."), + } + + def _auto_init(self, cr, context=None): + """Installation hook to create aliases for all lead and avoid constraint errors.""" + return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(crm_case_section, self)._auto_init, + 'crm.lead', self._columns['alias_id'], 'name', alias_prefix='Lead+', alias_defaults={}, context=context) + + def _get_stage_common(self, cr, uid, context): + ids = self.pool.get('crm.case.stage').search(cr, uid, [('case_default', '=', 1)], context=context) + return ids + + _defaults = { + 'stage_ids': _get_stage_common, + 'use_leads': True, + 'use_opportunities': True, + } + + def create(self, cr, uid, vals, context=None): + if context is None: + context = {} + create_context = dict(context, alias_model_name='crm.lead', alias_parent_model_name=self._name) + section_id = super(crm_case_section, self).create(cr, uid, vals, context=create_context) + section = self.browse(cr, uid, section_id, context=context) + self.pool.get('mail.alias').write(cr, uid, [section.alias_id.id], {'alias_parent_thread_id': section_id, 'alias_defaults': {'section_id': section_id, 'type': 'lead'}}, context=context) + return section_id + + def unlink(self, cr, uid, ids, context=None): + # Cascade-delete mail aliases as well, as they should not exist without the sales team. + mail_alias = self.pool.get('mail.alias') + alias_ids = [team.alias_id.id for team in self.browse(cr, uid, ids, context=context) if team.alias_id] + res = super(crm_case_section, self).unlink(cr, uid, ids, context=context) + mail_alias.unlink(cr, uid, alias_ids, context=context) + return res diff --git a/addons/crm/sales_team_view.xml b/addons/crm/sales_team_view.xml new file mode 100644 index 00000000000..7e0eb2e8ad6 --- /dev/null +++ b/addons/crm/sales_team_view.xml @@ -0,0 +1,185 @@ + + + + + + + + Leads + crm.lead + tree,form + ['|', ('type','=','lead'), ('type','=',False)] + + + { + 'search_default_section_id': [active_id], + 'default_section_id': active_id, + 'default_type': 'lead', + 'stage_type': 'lead', + } + + +

+ Use leads if you need a qualification step before creating an + opportunity or a customer. It can be a business card you received, + a contact form filled in your website, or a file of unqualified + prospects you import, etc. +

+ Once qualified, the lead can be converted into a business + opportunity and/or a new customer in your address book. +

+
+
+ + + + + Opportunities + crm.lead + kanban,tree,graph,form,calendar + [('type','=','opportunity')] + + + { + 'search_default_section_id': [active_id], + 'default_section_id': active_id, + 'stage_type': 'opportunity', + 'default_type': 'opportunity', + 'default_user_id': uid, + } + + +

+ OpenERP helps you keep track of your sales pipeline to follow + up potential sales and better forecast your future revenues. +

+ You will be able to plan meetings and phone calls from + opportunities, convert them into quotations, attach related + documents, track all discussions, and much more. +

+
+
+ + + Leads Analysis + crm.lead.report + {"search_default_month":1} + graph + + [('type','=', 'lead'),('section_id', '=', active_id)] + Leads Analysis allows you to check different CRM related information like the treatment delays or number of leads per state. You can sort out your leads analysis by different groups to get accurate grained analysis. + + + + Opportunities Analysis + crm.lead.report + graph + + [('type','=', 'opportunity'), ('section_id', '=', active_id)] + Opportunities Analysis gives you an instant access to your opportunities with information such as the expected revenue, planned cost, missed deadlines or the number of interactions per opportunity. This report is mainly used by the sales manager in order to do the periodic review with the teams of the sales pipeline. + + + + crm.case.section.kanban + crm.case.section + + + + + + + + + + + +
+ %% +
+ +
+
+
+
+ + + crm.case.section.form + crm.case.section + + + + + + + + + + + + + + + + + + + + + kanban + + + + + + + tree + + + + + + + form + + + + + + Cases by Sales Team + crm.case.section + [('parent_id','=',False)] + tree + + +
+
diff --git a/addons/crm/security/crm_security.xml b/addons/crm/security/crm_security.xml index f58be62393f..10477260d27 100644 --- a/addons/crm/security/crm_security.xml +++ b/addons/crm/security/crm_security.xml @@ -24,19 +24,6 @@
- - Do Not Use Sales Teams - - - - - - - - Manage Sales Teams - - - Manage Fund Raising diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv index 73237ecba6a..3c6fc05a70d 100644 --- a/addons/crm/security/ir.model.access.csv +++ b/addons/crm/security/ir.model.access.csv @@ -5,15 +5,12 @@ access_crm_segmentation,crm.segmentation,model_crm_segmentation,base.group_sale_ access_crm_segmentation_line,crm.segmentation.line,model_crm_segmentation_line,base.group_sale_manager,1,1,1,1 access_crm_case_channel_user,crm.case.channel user,model_crm_case_channel,base.group_sale_salesman,1,0,0,0 access_crm_case_channel_manager,crm.case.channel manager,model_crm_case_channel,base.group_sale_manager,1,1,1,1 -access_crm_case_section,crm.case.section,model_crm_case_section,base.group_user,1,0,0,0 access_crm_case_categ,crm.case.categ,model_crm_case_categ,base.group_sale_salesman,1,1,1,0 access_crm_lead_manager,crm.lead.manager,model_crm_lead,base.group_sale_manager,1,1,1,1 access_crm_phonecall_manager,crm.phonecall.manager,model_crm_phonecall,base.group_sale_manager,1,1,1,1 access_crm_case_categ,crm.case.categ,model_crm_case_categ,base.group_user,1,0,0,0 access_crm_lead,crm.lead,model_crm_lead,base.group_sale_salesman,1,1,1,0 access_crm_phonecall,crm.phonecall,model_crm_phonecall,base.group_sale_salesman,1,1,1,0 -access_crm_case_section_user,crm.case.section.user,model_crm_case_section,base.group_sale_salesman,1,0,0,0 -access_crm_case_section_manager,crm.case.section.manager,model_crm_case_section,base.group_sale_manager,1,1,1,1 access_crm_case_stage,crm.case.stage,model_crm_case_stage,,1,0,0,0 access_crm_case_stage_manager,crm.case.stage,model_crm_case_stage,base.group_sale_manager,1,1,1,1 access_crm_case_resource_type_user,crm_case_resource_type user,model_crm_case_resource_type,base.group_sale_salesman,1,1,1,0 diff --git a/addons/crm/static/src/css/Makefile b/addons/crm/static/src/css/Makefile deleted file mode 100644 index a2ced24a13d..00000000000 --- a/addons/crm/static/src/css/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -crm.css: crm.sass - sass --trace -t expanded crm.sass:crm.css diff --git a/addons/crm/test/crm_lead_cancel.yml b/addons/crm/test/crm_lead_cancel.yml index dd475d54919..eca85e4b7a1 100644 --- a/addons/crm/test/crm_lead_cancel.yml +++ b/addons/crm/test/crm_lead_cancel.yml @@ -5,7 +5,7 @@ uid: 'crm_res_users_salesmanager' - !python {model: crm.lead}: | - section_id = self.pool.get('crm.case.section').create(cr, uid, {'name': "Phone Marketing", 'parent_id': ref("crm.crm_case_section_2")}) + section_id = self.pool.get('crm.case.section').create(cr, uid, {'name': "Phone Marketing", 'parent_id': ref("sales_team.crm_case_section_2")}) self.write(cr, uid, [ref("crm_case_1")], {'section_id': section_id}) - Salesman check unqualified lead . diff --git a/addons/crm/test/crm_lead_find_stage.yml b/addons/crm/test/crm_lead_find_stage.yml index b8a11a4af9e..19bed36fc7e 100644 --- a/addons/crm/test/crm_lead_find_stage.yml +++ b/addons/crm/test/crm_lead_find_stage.yml @@ -6,7 +6,7 @@ name: 'Test lead new' partner_id: base.res_partner_1 description: This is the description of the test new lead. - section_id: crm.section_sales_department + section_id: sales_team.section_sales_department - I check default stage of lead. - diff --git a/addons/crm/test/lead2opportunity2win.yml b/addons/crm/test/lead2opportunity2win.yml index 0650838e4cc..1bae79bf983 100644 --- a/addons/crm/test/lead2opportunity2win.yml +++ b/addons/crm/test/lead2opportunity2win.yml @@ -65,7 +65,7 @@ - !python {model: crm.lead2opportunity.partner.mass}: | context.update({'active_model': 'crm.lead', 'active_ids': [ref("crm_case_13"), ref("crm_case_2")], 'active_id': ref("crm_case_13")}) - id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('base.user_root')])], 'section_id': ref('crm.section_sales_department')}, context=context) + id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('base.user_root')])], 'section_id': ref('sales_team.section_sales_department')}, context=context) self.mass_convert(cr, uid, [id], context=context) - Now I check first lead converted on opportunity. diff --git a/addons/crm/test/lead2opportunity_assign_salesmen.yml b/addons/crm/test/lead2opportunity_assign_salesmen.yml index 76b35be531d..9ac3e954f88 100644 --- a/addons/crm/test/lead2opportunity_assign_salesmen.yml +++ b/addons/crm/test/lead2opportunity_assign_salesmen.yml @@ -66,7 +66,7 @@ - !python {model: crm.lead2opportunity.partner.mass}: | context.update({'active_model': 'crm.lead', 'active_ids': [ref("test_crm_lead_01"), ref("test_crm_lead_02"), ref("test_crm_lead_03"), ref("test_crm_lead_04"), ref("test_crm_lead_05"), ref("test_crm_lead_06")], 'active_id': ref("test_crm_lead_01")}) - id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('test_res_user_01'), ref('test_res_user_02'), ref('test_res_user_03'), ref('test_res_user_04')])], 'section_id': ref('crm.section_sales_department'), 'deduplicate': False, 'force_assignation': True}, context=context) + id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('test_res_user_01'), ref('test_res_user_02'), ref('test_res_user_03'), ref('test_res_user_04')])], 'section_id': ref('sales_team.section_sales_department'), 'deduplicate': False, 'force_assignation': True}, context=context) self.mass_convert(cr, uid, [id], context=context) - The leads should now be opps with a salesman and a salesteam. Also, salesmen should have been assigned following a round-robin method. diff --git a/addons/crm/views/crm.xml b/addons/crm/views/crm.xml deleted file mode 100644 index c28ba723d24..00000000000 --- a/addons/crm/views/crm.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index 40654f464f4..7964e4a4a3a 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -276,7 +276,7 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory): active_ids = active_ids.difference(merged_lead_ids) active_ids = active_ids.union(remaining_lead_ids) ctx['active_ids'] = list(active_ids) - ctx['no_force_assignation'] = not data.force_assignation + ctx['no_force_assignation'] = context.get('no_force_assignation', not data.force_assignation) return self.action_apply(cr, uid, ids, context=ctx) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/wizard/crm_lead_to_opportunity_view.xml b/addons/crm/wizard/crm_lead_to_opportunity_view.xml index 0f8f676d92d..58bbcf4d934 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_view.xml +++ b/addons/crm/wizard/crm_lead_to_opportunity_view.xml @@ -12,7 +12,7 @@ - + diff --git a/addons/crm/wizard/crm_merge_opportunities_view.xml b/addons/crm/wizard/crm_merge_opportunities_view.xml index f88a6fe6f17..92007d52e42 100644 --- a/addons/crm/wizard/crm_merge_opportunities_view.xml +++ b/addons/crm/wizard/crm_merge_opportunities_view.xml @@ -10,7 +10,7 @@
- + diff --git a/addons/crm_claim/crm_claim_data.xml b/addons/crm_claim/crm_claim_data.xml index d897fe2bcb3..fca7ac51178 100644 --- a/addons/crm_claim/crm_claim_data.xml +++ b/addons/crm_claim/crm_claim_data.xml @@ -8,19 +8,19 @@ Factual Claims - + Value Claims - + Policy Claims - + @@ -30,12 +30,12 @@ Corrective - + Preventive - + diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 563ef6a991f..b7959832ead 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -580,7 +580,7 @@ class pos_order(osv.osv): try: self.signal_paid(cr, uid, [order_id]) except Exception as e: - _logger.error('Could not mark POS Order as Paid: %s', tools.ustr(e)) + _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) if to_invoice: self.action_invoice(cr, uid, [order_id], context) @@ -744,8 +744,6 @@ class pos_order(osv.osv): move_obj = self.pool.get('stock.move') for order in self.browse(cr, uid, ids, context=context): - if not order.state=='draft': - continue addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {} picking_type = order.picking_type_id picking_id = False diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 077a2399c55..0c7ba8f9e1e 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -146,6 +146,9 @@ function openerp_pos_db(instance, module){ var product = products[i]; var search_string = this._product_search_string(product); var categ_id = product.public_categ_id ? product.public_categ_id[0] : this.root_category_id; + if (product.variants){ + product.name = product.name+" ("+product.variants+")"; + } if(!stored_categories[categ_id]){ stored_categories[categ_id] = []; } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 16f9f7476b3..c0c376e240e 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -214,7 +214,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return self.fetch( 'product.product', - ['name', 'list_price','price','public_categ_id', 'taxes_id', 'ean13', 'default_code', + ['name', 'list_price','price','public_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'], [['sale_ok','=',true],['available_in_pos','=',true]], {pricelist: self.pricelist.id} // context for price diff --git a/addons/product/product.py b/addons/product/product.py index a7a027d0487..28045886464 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -895,14 +895,10 @@ class product_product(osv.osv): return res def copy(self, cr, uid, id, default=None, context=None): - context = context or {} - default = dict(default or {}) + if context is None: + context={} - # Craft our own ` (copy)` in en_US (self.copy_translation() - # will do the other languages). - context_wo_lang = dict(context or {}) - context_wo_lang.pop('lang', None) - product = self.browse(cr, uid, id, context_wo_lang) + product = self.browse(cr, uid, id, context) if context.get('variant'): # if we copy a variant or create one, we keep the same template default['product_tmpl_id'] = product.product_tmpl_id.id 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/project_issue/project_issue_demo.xml b/addons/project_issue/project_issue_demo.xml index 4ed2f090e24..7da36f1d4c6 100644 --- a/addons/project_issue/project_issue_demo.xml +++ b/addons/project_issue/project_issue_demo.xml @@ -38,7 +38,7 @@ 0 - + @@ -54,7 +54,7 @@ 1 - + Program not giving proper output @@ -66,7 +66,7 @@ 0 - + @@ -82,7 +82,7 @@ 1 - + @@ -95,7 +95,7 @@ 1 - + @@ -108,7 +108,7 @@ 1 - + @@ -124,7 +124,7 @@ 2 - + @@ -137,7 +137,7 @@ 2 - + @@ -162,7 +162,7 @@ 2 - + @@ -175,7 +175,7 @@ 0 - + @@ -188,7 +188,7 @@ 1 - + @@ -202,7 +202,7 @@ 1 - + @@ -216,7 +216,7 @@ 1 - + @@ -229,7 +229,7 @@ 0 - + @@ -242,7 +242,7 @@ 2 - + diff --git a/addons/project_issue/report/project_issue_report_view.xml b/addons/project_issue/report/project_issue_report_view.xml index ad6d6688d1e..b0e24d7db54 100644 --- a/addons/project_issue/report/project_issue_report_view.xml +++ b/addons/project_issue/report/project_issue_report_view.xml @@ -31,7 +31,7 @@ - + diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 95a1ea7636a..de3bbd63db6 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -39,7 +39,7 @@ class ReportController(Controller): @route([ '/report//', '/report///', - ], type='http', auth='user', website=True, multilang=True) + ], type='http', auth='user', website=True) def report_routes(self, reportname, docids=None, converter=None, **data): report_obj = request.registry['report'] cr, uid, context = request.cr, request.uid, request.context diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py index 5a7b4b499df..c99fc936467 100644 --- a/addons/sale/__init__.py +++ b/addons/sale/__init__.py @@ -19,15 +19,10 @@ # ############################################################################## -#---------------------------------------------------------- -# Init Sales -#---------------------------------------------------------- - import sale +import sales_team import res_partner import wizard import report import edi import res_config - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index 3b8eab814ab..039e765cd71 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -59,7 +59,7 @@ The Dashboard for the Sales Manager will include 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', 'images': ['images/Sale_order_line_to_invoice.jpeg','images/sale_order.jpeg','images/sales_analysis.jpeg'], - 'depends': ['account_voucher', 'procurement', 'report'], + 'depends': ['sales_team','account_voucher', 'procurement', 'report'], 'data': [ 'wizard/sale_make_invoice_advance.xml', 'wizard/sale_line_invoice.xml', @@ -71,6 +71,7 @@ The Dashboard for the Sales Manager will include 'sale_report.xml', 'sale_data.xml', 'sale_view.xml', + 'sales_team_view.xml', 'res_partner_view.xml', 'report/sale_report_view.xml', 'edi/sale_order_action_data.xml', diff --git a/addons/sale/edi/sale_order.py b/addons/sale/edi/sale_order.py index bf015b3cdfc..cd1b18ef9b5 100644 --- a/addons/sale/edi/sale_order.py +++ b/addons/sale/edi/sale_order.py @@ -22,7 +22,7 @@ from openerp.osv import osv, fields from openerp.addons.edi import EDIMixin from openerp.tools.translate import _ -from urllib import urlencode +from werkzeug import url_encode SALE_ORDER_LINE_EDI_STRUCT = { 'sequence': True, @@ -197,7 +197,7 @@ class sale_order(osv.osv, EDIMixin): "no_note": "1", "bn": "OpenERP_Order_PayNow_" + order.pricelist_id.currency_id.name, } - res[order.id] = "https://www.paypal.com/cgi-bin/webscr?" + urlencode(params) + res[order.id] = "https://www.paypal.com/cgi-bin/webscr?" + url_encode(params) return res _columns = { 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/sale/report/__init__.py b/addons/sale/report/__init__.py index 12d484c3e25..be1073205c3 100644 --- a/addons/sale/report/__init__.py +++ b/addons/sale/report/__init__.py @@ -20,5 +20,5 @@ ############################################################################## import sale_report - +import invoice_report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_crm/report/sales_crm_account_invoice_report.py b/addons/sale/report/invoice_report.py similarity index 100% rename from addons/sale_crm/report/sales_crm_account_invoice_report.py rename to addons/sale/report/invoice_report.py diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index e2f6f1e1cb2..7ea14ba1ee6 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -53,6 +53,7 @@ class sale_report(osv.osv): ], 'Order Status', readonly=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True), + 'section_id': fields.many2one('crm.case.section', 'Sales Team'), } _order = 'date desc' @@ -73,7 +74,8 @@ class sale_report(osv.osv): s.state, t.categ_id as categ_id, s.pricelist_id as pricelist_id, - s.project_id as analytic_account_id + s.project_id as analytic_account_id, + s.section_id as section_id """ return select_str @@ -101,7 +103,8 @@ class sale_report(osv.osv): s.company_id, s.state, s.pricelist_id, - s.project_id + s.project_id, + s.section_id """ return group_by_str diff --git a/addons/sale/report/sale_report_view.xml b/addons/sale/report/sale_report_view.xml index 14b719ebd7a..0549867ddec 100644 --- a/addons/sale/report/sale_report_view.xml +++ b/addons/sale/report/sale_report_view.xml @@ -21,8 +21,8 @@ - - + + @@ -34,6 +34,7 @@
+ diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 880f9811ffc..e3b59c00015 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -163,6 +163,28 @@ class sale_order(osv.osv): raise osv.except_osv(_('Error!'), _('There is no default company for the current user!')) return company_id + def _get_default_section_id(self, cr, uid, context=None): + """ Gives default section by checking if present in the context """ + section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False + if not section_id: + section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False + return section_id + + def _resolve_section_id_from_context(self, cr, uid, context=None): + """ Returns ID of section based on the value of 'section_id' + context key, or None if it cannot be resolved to a single + Sales Team. + """ + if context is None: + context = {} + if type(context.get('default_section_id')) in (int, long): + return context.get('default_section_id') + if isinstance(context.get('default_section_id'), basestring): + section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context) + if len(section_ids) == 1: + return int(section_ids[0][0]) + return None + _columns = { 'name': fields.char('Order Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), @@ -228,7 +250,9 @@ class sale_order(osv.osv): 'payment_term': fields.many2one('account.payment.term', 'Payment Term'), 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'), 'company_id': fields.many2one('res.company', 'Company'), + 'section_id': fields.many2one('crm.case.section', 'Sales Team'), 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group'), + } _defaults = { 'date_order': fields.datetime.now, @@ -239,7 +263,8 @@ class sale_order(osv.osv): 'name': lambda obj, cr, uid, context: '/', 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'], 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'], - 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note + 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note, + 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c), } _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), @@ -374,7 +399,8 @@ class sale_order(osv.osv): 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id, 'date_invoice': context.get('date_invoice', False), 'company_id': order.company_id.id, - 'user_id': order.user_id and order.user_id.id or False + 'user_id': order.user_id and order.user_id.id or False, + 'section_id' : order.section_id.id } # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1 @@ -1189,6 +1215,36 @@ class mail_compose_message(osv.Model): class account_invoice(osv.Model): _inherit = 'account.invoice' + def _get_default_section_id(self, cr, uid, context=None): + """ Gives default section by checking if present in the context """ + section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False + if not section_id: + section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False + return section_id + + def _resolve_section_id_from_context(self, cr, uid, context=None): + """ Returns ID of section based on the value of 'section_id' + context key, or None if it cannot be resolved to a single + Sales Team. + """ + if context is None: + context = {} + if type(context.get('default_section_id')) in (int, long): + return context.get('default_section_id') + if isinstance(context.get('default_section_id'), basestring): + section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context) + if len(section_ids) == 1: + return int(section_ids[0][0]) + return None + + _columns = { + 'section_id': fields.many2one('crm.case.section', 'Sales Team'), + } + + _defaults = { + 'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c) + } + def confirm_paid(self, cr, uid, ids, context=None): sale_order_obj = self.pool.get('sale.order') res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context) diff --git a/addons/sale/sale_data.xml b/addons/sale/sale_data.xml index 443895631a9..4c5347cf1d0 100644 --- a/addons/sale/sale_data.xml +++ b/addons/sale/sale_data.xml @@ -29,7 +29,7 @@ This application lets you create and send quotations and process your sales orders; from delivery to invoicing.

If you need to manage your sales pipeline (leads, opportunities, phonecalls), the CRM application may be useful. Use the Settings menu to install it.

]]>
- + Quotation sent @@ -44,5 +44,21 @@ Quotation confirmed + + + Quotation Send + 20 + crm.case.section + + section_id + + + Sales Order Confirmed + 21 + crm.case.section + + section_id + + diff --git a/addons/sale/sale_demo.xml b/addons/sale/sale_demo.xml index 2e0019598f7..bcb69f9a7a3 100644 --- a/addons/sale/sale_demo.xml +++ b/addons/sale/sale_demo.xml @@ -2,12 +2,24 @@ + + 52700 + 60000 + + + + 36000 + 40000 + + + + @@ -47,6 +59,8 @@ manual + + @@ -76,6 +90,7 @@ manual + @@ -104,6 +119,7 @@ + @@ -152,6 +168,8 @@ + + @@ -190,6 +208,7 @@ + @@ -209,6 +228,8 @@ manual + + @@ -259,6 +280,7 @@ manual + @@ -335,5 +357,86 @@ Thanks!
+ + + + + + + + draft + in_invoice + + Test invoice 1 + + + Basic computer with Dvorak keyboard and left-handed mouse + + 250 + 1 + + + + Little server with raid 1 and 512ECC ram + + 800 + 2 + + + + Server with raid 10 and 2048ECC ram + + 4800 + 4 + + + + + + + + + + + + + + + + + + + + + + + + + draft + out_invoice + + Test invoice 1 + + + Basic formation with Dvorak + + 500 + 1 + + + + + + + + + + + + + + + + diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 402737b4f01..25163d9e5b8 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -201,6 +201,7 @@ + @@ -255,12 +256,14 @@ + + @@ -519,5 +522,163 @@ + + + + Account Invoice + account.invoice + + + + + + + + + + + + + account.invoice.groupby + account.invoice + + + + + + + + + + + + + + Account Invoice + account.invoice + + + + + + + + + + + + + Sales Orders + ir.actions.act_window + sale.order + form + tree,form,calendar,graph + + [('state','not in',('draft','sent','cancel'))] + { + 'search_default_section_id': [active_id], + 'default_section_id': active_id, + } + + +

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

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

+
+
+ + + Quotations + ir.actions.act_window + sale.order + form + + tree,form,calendar,graph + { + 'search_default_section_id': [active_id], + 'default_section_id': active_id, + 'show_address': 1, + } + + [('state','in',('draft','sent','cancel'))] + + +

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

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

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

+
+
+ + + Invoices + account.invoice + form + tree,form,calendar,graph + + [ + ('state', 'not in', ['draft', 'cancel']), + ('type', '=', 'out_invoice')] + { + 'search_default_section_id': [active_id], + 'default_section_id': active_id, + 'default_type':'out_invoice', + 'type':'out_invoice', + 'journal_type': 'sale', + } + + + + + + 1 + tree + + + + + 2 + form + + + + + + Quotations Analysis + sale.report + graph + [('state','=','draft'),('section_id', '=', active_id)] + {'search_default_order_month':1} + This report performs analysis on your quotations. 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. + + + + Sales Analysis + sale.report + graph + [('state','not in',('draft','sent','cancel')),('section_id', '=', active_id)] + {'search_default_order_month':1} + This report performs analysis on your 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. + + + + Invoices Analysis + account.invoice.report + graph + [('section_id', '=', active_id),('state', 'not in', ['draft', 'cancel'])] + {'search_default_month':1} + From this report, you can have an overview of the amount invoiced to your customer. The tool search can also be used to personalise your Invoices reports and so, match this analysis to your needs. + diff --git a/addons/sale/sales_team.py b/addons/sale/sales_team.py new file mode 100644 index 00000000000..ba9f3e3f5e1 --- /dev/null +++ b/addons/sale/sales_team.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +import calendar +from datetime import date +from dateutil import relativedelta + +from openerp import tools +from openerp.osv import fields, osv + + +class crm_case_section(osv.osv): + _inherit = 'crm.case.section' + + def _get_sale_orders_data(self, cr, uid, ids, field_name, arg, context=None): + obj = self.pool.get('sale.order') + res = dict.fromkeys(ids, False) + month_begin = date.today().replace(day=1) + date_begin = (month_begin - relativedelta.relativedelta(months=self._period_number - 1)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + for id in ids: + res[id] = dict() + created_domain = [('section_id', '=', id), ('state', '=', 'draft'), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)] + res[id]['monthly_quoted'] = self.__get_bar_values(cr, uid, obj, created_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context) + validated_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'sent', 'cancel']), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)] + res[id]['monthly_confirmed'] = self.__get_bar_values(cr, uid, obj, validated_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context) + return res + + def _get_invoices_data(self, cr, uid, ids, field_name, arg, context=None): + obj = self.pool.get('account.invoice.report') + res = dict.fromkeys(ids, False) + month_begin = date.today().replace(day=1) + date_begin = (month_begin - relativedelta.relativedelta(months=self._period_number - 1)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) + for id in ids: + created_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'cancel']), ('date', '>=', date_begin), ('date', '<=', date_end)] + res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['price_total', 'date'], 'price_total', 'date', context=context) + return res + + _columns = { + 'use_quotations': fields.boolean('Opportunities', help="Check this box to manage quotations in this sales team."), + 'invoiced_forecast': fields.integer(string='Invoice Forecast', + help="Forecast of the invoice revenue for the current month. This is the amount the sales \n" + "team should invoice this month. It is used to compute the progression ratio \n" + " of the current and forecast revenue on the kanban view."), + 'invoiced_target': fields.integer(string='Invoice Target', + help="Target of invoice revenue for the current month. This is the amount the sales \n" + "team estimates to be able to invoice this month."), + 'monthly_quoted': fields.function(_get_sale_orders_data, + type='string', readonly=True, multi='_get_sale_orders_data', + string='Rate of created quotation per duration'), + 'monthly_confirmed': fields.function(_get_sale_orders_data, + type='string', readonly=True, multi='_get_sale_orders_data', + string='Rate of validate sales orders per duration'), + 'monthly_invoiced': fields.function(_get_invoices_data, + type='string', readonly=True, + string='Rate of sent invoices per duration'), + } + + _defaults = { + 'use_quotations': True, + } + + def action_forecast(self, cr, uid, id, value, context=None): + return self.write(cr, uid, [id], {'invoiced_forecast': round(float(value))}, context=context) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale/sales_team_view.xml b/addons/sale/sales_team_view.xml new file mode 100644 index 00000000000..922caa9f033 --- /dev/null +++ b/addons/sale/sales_team_view.xml @@ -0,0 +1,78 @@ + + + + + crm.case.section.form + crm.case.section + + + + + + + + + + + + + + + crm.case.section.kanban + crm.case.section + + + + + + + + + + + + + + + +
+ Invoiced + Forecast +
+
+
Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance. +
+
+
+
+
+
+
diff --git a/addons/sale_crm/__init__.py b/addons/sale_crm/__init__.py index cda0cd795a2..ac44586e7db 100644 --- a/addons/sale_crm/__init__.py +++ b/addons/sale_crm/__init__.py @@ -21,6 +21,5 @@ import wizard import sale_crm -import report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_crm/__openerp__.py b/addons/sale_crm/__openerp__.py index 600191b2f92..251c4746a86 100644 --- a/addons/sale_crm/__openerp__.py +++ b/addons/sale_crm/__openerp__.py @@ -41,12 +41,10 @@ modules. 'data': [ 'wizard/crm_make_sale_view.xml', 'sale_crm_view.xml', - 'sale_crm_data.xml', 'security/sale_crm_security.xml', 'security/ir.model.access.csv', - 'report/sale_report_view.xml', ], - 'demo': ['sale_crm_demo.xml'], + 'demo': [], 'test': ['test/sale_crm.yml'], 'installable': True, 'auto_install': True, diff --git a/addons/sale_crm/report/sale_report.py b/addons/sale_crm/report/sale_report.py deleted file mode 100644 index eca020f82ad..00000000000 --- a/addons/sale_crm/report/sale_report.py +++ /dev/null @@ -1,18 +0,0 @@ - # -*- coding: utf-8 -*- - -from openerp.osv import fields, osv - - -class sale_report(osv.osv): - _inherit = "sale.report" - _columns = { - 'section_id': fields.many2one('crm.case.section', 'Sales Team'), - } - - def _select(self): - return super(sale_report, self)._select() + ", s.section_id as section_id" - - def _group_by(self): - return super(sale_report, self)._group_by() + ", s.section_id" - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_crm/report/sale_report_view.xml b/addons/sale_crm/report/sale_report_view.xml deleted file mode 100644 index fb62468ec91..00000000000 --- a/addons/sale_crm/report/sale_report_view.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - sale.report.search.sale.crm - sale.report - - - - - - - - - - \ No newline at end of file diff --git a/addons/sale_crm/sale_crm.py b/addons/sale_crm/sale_crm.py index 739b70c8588..da6fde90300 100644 --- a/addons/sale_crm/sale_crm.py +++ b/addons/sale_crm/sale_crm.py @@ -26,117 +26,9 @@ from dateutil import relativedelta from openerp import tools from openerp.osv import osv, fields -class res_users(osv.Model): - _inherit = 'res.users' - _columns = { - 'default_section_id': fields.many2one('crm.case.section', 'Default Sales Team'), - } - - def __init__(self, pool, cr): - init_res = super(res_users, self).__init__(pool, cr) - # duplicate list to avoid modifying the original reference - self.SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) - self.SELF_WRITEABLE_FIELDS.extend(['default_section_id']) - return init_res - class sale_order(osv.osv): _inherit = 'sale.order' _columns = { - 'section_id': fields.many2one('crm.case.section', 'Sales Team'), 'categ_ids': fields.many2many('crm.case.categ', 'sale_order_category_rel', 'order_id', 'category_id', 'Tags', \ domain="['|', ('section_id', '=', section_id), ('section_id', '=', False), ('object_id.model', '=', 'crm.lead')]", context="{'object_name': 'crm.lead'}") } - - def _get_default_section_id(self, cr, uid, context=None): - """ Gives default section by checking if present in the context """ - section_id = self.pool.get('crm.lead')._resolve_section_id_from_context(cr, uid, context=context) or False - if not section_id: - section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False - return section_id - - _defaults = { - 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c), - } - - def _prepare_invoice(self, cr, uid, order, lines, context=None): - invoice_vals = super(sale_order, self)._prepare_invoice(cr, uid, order, lines, context=context) - if order.section_id and order.section_id.id: - invoice_vals['section_id'] = order.section_id.id - return invoice_vals - - -class crm_case_section(osv.osv): - _inherit = 'crm.case.section' - - def _get_sale_orders_data(self, cr, uid, ids, field_name, arg, context=None): - obj = self.pool.get('sale.order') - res = dict.fromkeys(ids, False) - month_begin = date.today().replace(day=1) - date_begin = (month_begin - relativedelta.relativedelta(months=self._period_number - 1)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - for id in ids: - res[id] = dict() - created_domain = [('section_id', '=', id), ('state', '=', ['draft']), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)] - res[id]['monthly_quoted'] = self.__get_bar_values(cr, uid, obj, created_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context) - validated_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'sent', 'cancel']), ('date_order', '>=', date_begin), ('date_order', '<=', date_end)] - res[id]['monthly_confirmed'] = self.__get_bar_values(cr, uid, obj, validated_domain, ['amount_total', 'date_order'], 'amount_total', 'date_order', context=context) - return res - - def _get_invoices_data(self, cr, uid, ids, field_name, arg, context=None): - obj = self.pool.get('account.invoice.report') - res = dict.fromkeys(ids, False) - month_begin = date.today().replace(day=1) - date_begin = (month_begin - relativedelta.relativedelta(months=self._period_number - 1)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - date_end = month_begin.replace(day=calendar.monthrange(month_begin.year, month_begin.month)[1]).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - for id in ids: - created_domain = [('section_id', '=', id), ('state', 'not in', ['draft', 'cancel']), ('date', '>=', date_begin), ('date', '<=', date_end)] - res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['price_total', 'date'], 'price_total', 'date', context=context) - return res - - _columns = { - 'invoiced_forecast': fields.integer(string='Invoice Forecast', - help="Forecast of the invoice revenue for the current month. This is the amount the sales \n" - "team should invoice this month. It is used to compute the progression ratio \n" - " of the current and forecast revenue on the kanban view."), - 'invoiced_target': fields.integer(string='Invoice Target', - help="Target of invoice revenue for the current month. This is the amount the sales \n" - "team estimates to be able to invoice this month."), - 'monthly_quoted': fields.function(_get_sale_orders_data, - type='string', readonly=True, multi='_get_sale_orders_data', - string='Rate of created quotation per duration'), - 'monthly_confirmed': fields.function(_get_sale_orders_data, - type='string', readonly=True, multi='_get_sale_orders_data', - string='Rate of validate sales orders per duration'), - 'monthly_invoiced': fields.function(_get_invoices_data, - type='string', readonly=True, - string='Rate of sent invoices per duration'), - } - - def action_forecast(self, cr, uid, id, value, context=None): - return self.write(cr, uid, [id], {'invoiced_forecast': round(float(value))}, context=context) - -class sale_crm_lead(osv.Model): - _inherit = 'crm.lead' - - def on_change_user(self, cr, uid, ids, user_id, context=None): - """ Override of on change user_id on lead/opportunity; when having sale - the new logic is : - - use user.default_section_id - - or fallback on previous behavior """ - if user_id: - user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) - if user.default_section_id and user.default_section_id.id: - return {'value': {'section_id': user.default_section_id.id}} - return super(sale_crm_lead, self).on_change_user(cr, uid, ids, user_id, context=context) - - -class account_invoice(osv.osv): - _inherit = 'account.invoice' - _columns = { - 'section_id': fields.many2one('crm.case.section', 'Sales Team'), - } - _defaults = { - 'section_id': lambda self, cr, uid, c=None: self.pool.get('res.users').browse(cr, uid, uid, c).default_section_id.id or False, - } - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_crm/sale_crm_data.xml b/addons/sale_crm/sale_crm_data.xml deleted file mode 100644 index 1996f367202..00000000000 --- a/addons/sale_crm/sale_crm_data.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - Quotation Send - 20 - crm.case.section - - section_id - - - Sales Order Confirmed - 21 - crm.case.section - - section_id - - - - diff --git a/addons/sale_crm/sale_crm_demo.xml b/addons/sale_crm/sale_crm_demo.xml deleted file mode 100644 index 25f653548d1..00000000000 --- a/addons/sale_crm/sale_crm_demo.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - 52700 - 60000 - - - - Indirect Sales - IM - 36000 - 40000 - - - - - - - - - - - draft - in_invoice - - Test invoice 1 - - - Basic computer with Dvorak keyboard and left-handed mouse - - 250 - 1 - - - - Little server with raid 1 and 512ECC ram - - 800 - 2 - - - - Server with raid 10 and 2048ECC ram - - 4800 - 4 - - - - - - - - - - - - - - - - - - - - - - - - - draft - out_invoice - - Test invoice 1 - - - Basic formation with Dvorak - - 500 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml index 88d7fe8be6c..070b8ef4fa7 100644 --- a/addons/sale_crm/sale_crm_view.xml +++ b/addons/sale_crm/sale_crm_view.xml @@ -27,285 +27,9 @@ - - - - sale.order.list.select - sale.order - - - - - - - - - - - - - - - Account Invoice - account.invoice - - - - - - - - - - - - - account.invoice.groupby - account.invoice - - - - - - - - - - - - - - Account Invoice - account.invoice - - - - - - - - - - - - - Users Preferences - res.users - - - - - - - - - - - - - res.users.preferences.form - res.users - - - - - - - - - - - - - - Sales Orders - ir.actions.act_window - sale.order - form - tree,form,calendar,graph - - [('state','not in',('draft','sent','cancel'))] - { - 'search_default_section_id': [active_id], - 'default_section_id': active_id, - } - - -

- Click to create a quotation that can be converted into a sales - order. -

- OpenERP will help you efficiently handle the complete sales flow: - quotation, sales order, delivery, invoicing and payment. -

-
-
- - - Quotations - ir.actions.act_window - sale.order - form - - tree,form,calendar,graph - { - 'search_default_section_id': [active_id], - 'default_section_id': active_id, - 'show_address': 1, - } - - [('state','in',('draft','sent','cancel'))] - - -

- Click to create a quotation, the first step of a new sale. -

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

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

-
-
- - - Invoices - account.invoice - form - tree,form,calendar,graph - - [ - ('state', 'not in', ['draft', 'cancel']), - ('type', '=', 'out_invoice')] - { - 'search_default_section_id': [active_id], - 'default_section_id': active_id, - 'default_type':'out_invoice', - 'type':'out_invoice', - 'journal_type': 'sale', - } - - - - - - 1 - tree - - - - - 2 - form - - - - - - Quotations Analysis - sale.report - graph - [('state','=','draft'),('section_id', '=', active_id)] - {'search_default_order_month':1} - This report performs analysis on your quotations. 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. - - - - Sales Analysis - sale.report - graph - [('state','not in',('draft','sent','cancel')),('section_id', '=', active_id)] - {'search_default_order_month':1} - This report performs analysis on your 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. - - - - Invoices Analysis - account.invoice.report - graph - [('section_id', '=', active_id),('state', 'not in', ['draft', 'cancel'])] - {'search_default_month':1} - From this report, you can have an overview of the amount invoiced to your customer. The tool search can also be used to personalise your Invoices reports and so, match this analysis to your needs. - - - - crm.case.section.form - crm.case.section - - - - - - - - - - - - - crm.case.section.kanban - crm.case.section - - - - - - - - - - - - - - - - - - -
- Invoiced - Forecast -
-
-
Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance. -
-
-
-
-
diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 0020ebf1d8e..65158582eb0 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -358,12 +358,12 @@ class sale_order_line(osv.osv): uom = False if not uom2: uom2 = product_obj.uom_id - compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding) + compare_qty = float_compare(product_obj.virtual_available, qty, precision_rounding=uom2.rounding) if (product_obj.type=='product') and int(compare_qty) == -1: warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \ - (qty, uom2 and uom2.name or product_obj.uom_id.name, - max(0,product_obj.virtual_available), product_obj.uom_id.name, - max(0,product_obj.qty_available), product_obj.uom_id.name) + (qty, uom2.name, + max(0,product_obj.virtual_available), uom2.name, + max(0,product_obj.qty_available), uom2.name) warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n" #update of warning messages diff --git a/addons/sale_crm/report/__init__.py b/addons/sales_team/__init__.py similarity index 94% rename from addons/sale_crm/report/__init__.py rename to addons/sales_team/__init__.py index d86b3e66de9..3b2a8f41267 100644 --- a/addons/sale_crm/report/__init__.py +++ b/addons/sales_team/__init__.py @@ -18,9 +18,7 @@ # along with this program. If not, see . # ############################################################################## - -import sales_crm_account_invoice_report -import sale_report +import sales_team +import res_config # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/sales_team/__openerp__.py b/addons/sales_team/__openerp__.py new file mode 100644 index 00000000000..c9cb20cd401 --- /dev/null +++ b/addons/sales_team/__openerp__.py @@ -0,0 +1,43 @@ +# -*- 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 . +# +############################################################################## +{ + 'name': 'Sale Team', + 'version': '1.0', + 'author': 'OpenERP SA', + 'category': 'Sales Management', + 'summary': 'Sales Team', + 'description': """ +Using this application you can manage Sales Team with CRM and/or Sales +======================================================================= + """, + 'website': 'http://www.openerp.com', + 'depends': ['base','mail','web_kanban_sparkline',], + 'data': ['security/sales_team_security.xml', + 'security/ir.model.access.csv', + 'res_config_view.xml', + 'sales_team_data.xml', + 'sales_team.xml',], + 'demo': ['sales_team_demo.xml'], + 'css': ['static/src/css/sales_team.css'], + 'installable': True, + 'auto_install': True, +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sales_team/res_config.py b/addons/sales_team/res_config.py new file mode 100644 index 00000000000..3081a2e918d --- /dev/null +++ b/addons/sales_team/res_config.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import fields, osv + + +class sales_team_configuration(osv.TransientModel): + _name = 'sale.config.settings' + _inherit = ['sale.config.settings'] + + def set_group_multi_salesteams(self, cr, uid, ids, context=None): + """ This method is automatically called by res_config as it begins + with set. It is used to implement the 'one group or another' + behavior. We have to perform some group manipulation by hand + because in res_config.execute(), set_* methods are called + after group_*; therefore writing on an hidden res_config file + could not work. + If group_multi_salesteams is checked: remove group_mono_salesteams + from group_user, remove the users. Otherwise, just add + group_mono_salesteams in group_user. + The inverse logic about group_multi_salesteams is managed by the + normal behavior of 'group_multi_salesteams' field. + """ + def ref(xml_id): + mod, xml = xml_id.split('.', 1) + return self.pool['ir.model.data'].get_object(cr, uid, mod, xml, context) + + for obj in self.browse(cr, uid, ids, context=context): + config_group = ref('base.group_mono_salesteams') + base_group = ref('base.group_user') + if obj.group_multi_salesteams: + base_group.write({'implied_ids': [(3, config_group.id)]}) + config_group.write({'users': [(3, u.id) for u in base_group.users]}) + else: + base_group.write({'implied_ids': [(4, config_group.id)]}) + return True + + _columns = { + 'group_multi_salesteams': fields.boolean("Organize Sales activities into multiple Sales Teams", + implied_group='base.group_multi_salesteams', + help="""Allows you to use Sales Teams to manage your leads and opportunities."""), + } diff --git a/addons/sales_team/res_config_view.xml b/addons/sales_team/res_config_view.xml new file mode 100644 index 00000000000..88a51e78cda --- /dev/null +++ b/addons/sales_team/res_config_view.xml @@ -0,0 +1,28 @@ + + + + + crm settings + sale.config.settings + + + +
+ + + +
+
+
+
+ +
+
diff --git a/addons/sales_team/sales_team.py b/addons/sales_team/sales_team.py new file mode 100644 index 00000000000..ed3a3876231 --- /dev/null +++ b/addons/sales_team/sales_team.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-today 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 . +# +############################################################################## +from datetime import date, datetime +from dateutil import relativedelta +from openerp import tools +from openerp.osv import fields, osv + + +class crm_case_section(osv.osv): + _name = "crm.case.section" + _inherit = ['mail.thread', 'ir.needaction_mixin'] + _description = "Sales Teams" + _order = "complete_name" + _period_number = 5 + + def get_full_name(self, cr, uid, ids, field_name, arg, context=None): + return dict(self.name_get(cr, uid, ids, context=context)) + + def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None): + """ Generic method to generate data for bar chart values using SparklineBarWidget. + This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field). + + :param obj: the target model (i.e. crm_lead) + :param domain: the domain applied to the read_group + :param list read_fields: the list of fields to read in the read_group + :param str value_field: the field used to compute the value of the bar slice + :param str groupby_field: the fields used to group + + :return list section_result: a list of dicts: [ + { 'value': (int) bar_column_value, + 'tootip': (str) bar_column_tooltip, + } + ] + """ + month_begin = date.today().replace(day=1) + section_result = [{ + 'value': 0, + 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B %Y'), + } for i in range(self._period_number - 1, -1, -1)] + group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context) + pattern = tools.DEFAULT_SERVER_DATE_FORMAT if obj.fields_get(cr, uid, groupby_field)[groupby_field]['type'] == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT + for group in group_obj: + group_begin_date = datetime.strptime(group['__domain'][0][2], pattern) + month_delta = relativedelta.relativedelta(month_begin, group_begin_date) + section_result[self._period_number - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field, 0)} + return section_result + + _columns = { + 'name': fields.char('Sales Team', size=64, required=True, translate=True), + 'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True), + 'code': fields.char('Code', size=8), + 'active': fields.boolean('Active', help="If the active field is set to "\ + "true, it will allow you to hide the sales team without removing it."), + 'change_responsible': fields.boolean('Reassign Escalated', help="When escalating to this team override the salesman with the team leader."), + 'user_id': fields.many2one('res.users', 'Team Leader'), + 'member_ids': fields.many2many('res.users', 'sale_member_rel', 'section_id', 'member_id', 'Team Members'), + 'reply_to': fields.char('Reply-To', size=64, help="The email address put in the 'Reply-To' of all emails sent by OpenERP about cases in this sales team"), + 'parent_id': fields.many2one('crm.case.section', 'Parent Team'), + 'child_ids': fields.one2many('crm.case.section', 'parent_id', 'Child Teams'), + 'note': fields.text('Description'), + 'working_hours': fields.float('Working Hours', digits=(16, 2)), + 'color': fields.integer('Color Index'), + } + + _defaults = { + 'active': 1, + } + + _sql_constraints = [ + ('code_uniq', 'unique (code)', 'The code of the sales team must be unique !') + ] + + _constraints = [ + (osv.osv._check_recursion, 'Error ! You cannot create recursive Sales team.', ['parent_id']) + ] + + def name_get(self, cr, uid, ids, context=None): + """Overrides orm name_get method""" + if not isinstance(ids, list): + ids = [ids] + res = [] + if not ids: + return res + reads = self.read(cr, uid, ids, ['name', 'parent_id'], context) + + for record in reads: + name = record['name'] + if record['parent_id']: + name = record['parent_id'][1] + ' / ' + name + res.append((record['id'], name)) + return res + + +class res_partner(osv.Model): + _inherit = 'res.partner' + _columns = { + 'section_id': fields.many2one('crm.case.section', 'Sales Team'), + } + + +class res_users(osv.Model): + _inherit = 'res.users' + _columns = { + 'default_section_id': fields.many2one('crm.case.section', 'Default Sales Team'), + } + + def __init__(self, pool, cr): + init_res = super(res_users, self).__init__(pool, cr) + # duplicate list to avoid modifying the original reference + self.SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) + self.SELF_WRITEABLE_FIELDS.extend(['default_section_id']) + return init_res diff --git a/addons/sales_team/sales_team.xml b/addons/sales_team/sales_team.xml new file mode 100644 index 00000000000..5ac8e24d8bb --- /dev/null +++ b/addons/sales_team/sales_team.xml @@ -0,0 +1,211 @@ + + + + + + + Users Preferences + res.users + + + + + + + + + + + + + res.users.preferences.form + res.users + + + + + + + + + + + + + + crm.case.section.kanban + crm.case.section + + + + + + + + + +
+
+ í +
+
+

+
+
+ + + +
+
+
+
+
+
+
+
+ + + + Case Sections - Search + crm.case.section + + + + + + + + + + + + + + + + + + + Sales Teams + crm.case.section + form + kanban,tree,form + {} + + +

+ Click here to define a new sales team. +

+ Use sales team to organize your different salespersons or + departments into separate teams. Each team will work in + its own list of opportunities. +

+
+
+ + + + + crm.case.section.form + crm.case.section + + + +
+
+ + + + + + + + + + + + + + + + +
+ X +
+
+ +
+
+
+
+
+
+
+
+ + + +
+
+
+ + +
+ +
+
+ + + + crm.case.section.tree + crm.case.section + child_ids + + + + + + + + + + + Sales Teams + crm.case.section + form + + +

+ Click here to define a new sales team. +

+ Use sales team to organize your different salespersons or + departments into separate teams. Each team will work in + its own list of opportunities. +

+
+
+ + + + + + +
+
diff --git a/addons/sales_team/sales_team_data.xml b/addons/sales_team/sales_team_data.xml new file mode 100644 index 00000000000..2f296143ec0 --- /dev/null +++ b/addons/sales_team/sales_team_data.xml @@ -0,0 +1,10 @@ + + + + + Direct Sales + DM + + + + diff --git a/addons/sales_team/sales_team_demo.xml b/addons/sales_team/sales_team_demo.xml new file mode 100644 index 00000000000..0e698d8f1fa --- /dev/null +++ b/addons/sales_team/sales_team_demo.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + Indirect Sales + IM + + + + + Marketing + SPD + + + + \ No newline at end of file diff --git a/addons/sales_team/security/ir.model.access.csv b/addons/sales_team/security/ir.model.access.csv new file mode 100644 index 00000000000..f983a2abb9d --- /dev/null +++ b/addons/sales_team/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_crm_case_section,crm.case.section,model_crm_case_section,base.group_user,1,0,0,0 +access_crm_case_section_user,crm.case.section.user,model_crm_case_section,base.group_sale_salesman,1,0,0,0 +access_crm_case_section_manager,crm.case.section.manager,model_crm_case_section,base.group_sale_manager,1,1,1,1 diff --git a/addons/sales_team/security/sales_team_security.xml b/addons/sales_team/security/sales_team_security.xml new file mode 100644 index 00000000000..d9002f8332f --- /dev/null +++ b/addons/sales_team/security/sales_team_security.xml @@ -0,0 +1,17 @@ + + + + + Do Not Use Sales Teams + + + + + + + + Manage Sales Teams + + + + diff --git a/addons/sales_team/static/src/css/Makefile b/addons/sales_team/static/src/css/Makefile new file mode 100644 index 00000000000..523f4a1e81e --- /dev/null +++ b/addons/sales_team/static/src/css/Makefile @@ -0,0 +1,2 @@ +sales_team.css: sales_team.sass + sass --trace -t expanded sales_team.sass:sales_team.css diff --git a/addons/crm/static/src/css/crm.css b/addons/sales_team/static/src/css/sales_team.css similarity index 100% rename from addons/crm/static/src/css/crm.css rename to addons/sales_team/static/src/css/sales_team.css diff --git a/addons/crm/static/src/css/crm.sass b/addons/sales_team/static/src/css/sales_team.sass similarity index 100% rename from addons/crm/static/src/css/crm.sass rename to addons/sales_team/static/src/css/sales_team.sass diff --git a/addons/crm/static/src/js/crm_case_section.js b/addons/sales_team/static/src/js/sales_team.js similarity index 100% rename from addons/crm/static/src/js/crm_case_section.js rename to addons/sales_team/static/src/js/sales_team.js diff --git a/addons/stock_picking_wave/controllers/main.py b/addons/stock_picking_wave/controllers/main.py index 9de14934828..8aab4905933 100644 --- a/addons/stock_picking_wave/controllers/main.py +++ b/addons/stock_picking_wave/controllers/main.py @@ -4,7 +4,7 @@ from openerp.addons.web.http import request class picking_wave_report(http.Controller): @http.route('/report/stock_picking_wave.report_pickingwave/', type='http', auth='user', - website=True, multilang=True) + website=True) def report_picking_wave(self, ids): self.cr, self.uid, self.pool = request.cr, request.uid, request.registry ids = [int(i) for i in ids.split(',')] diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 69fa12222c5..876f444ea57 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -76,7 +76,7 @@ class WebsiteSurvey(http.Controller): # Survey start @http.route(['/survey/start/', '/survey/start//'], - type='http', auth='public', multilang=True, website=True) + type='http', auth='public', website=True) def start_survey(self, survey, token=None, **post): cr, uid, context = request.cr, request.uid, request.context survey_obj = request.registry['survey.survey'] @@ -123,7 +123,7 @@ class WebsiteSurvey(http.Controller): # Survey displaying @http.route(['/survey/fill//', '/survey/fill///'], - type='http', auth='public', multilang=True, website=True) + type='http', auth='public', website=True) def fill_survey(self, survey, token, prev=None, **post): '''Display and validates a survey''' cr, uid, context = request.cr, request.uid, request.context @@ -173,7 +173,7 @@ class WebsiteSurvey(http.Controller): # AJAX prefilling of a survey @http.route(['/survey/prefill//', '/survey/prefill///'], - type='http', auth='public', multilang=True, website=True) + type='http', auth='public', website=True) def prefill(self, survey, token, page=None, **post): cr, uid, context = request.cr, request.uid, request.context user_input_line_obj = request.registry['survey.user_input_line'] @@ -216,7 +216,7 @@ class WebsiteSurvey(http.Controller): # AJAX scores loading for quiz correction mode @http.route(['/survey/scores//'], - type='http', auth='public', multilang=True, website=True) + type='http', auth='public', website=True) def get_scores(self, survey, token, page=None, **post): cr, uid, context = request.cr, request.uid, request.context user_input_line_obj = request.registry['survey.user_input_line'] @@ -234,7 +234,7 @@ class WebsiteSurvey(http.Controller): # AJAX submission of a page @http.route(['/survey/submit/'], - type='http', auth='public', multilang=True, website=True) + type='http', methods=['POST'], auth='public', website=True) def submit(self, survey, **post): _logger.debug('Incoming data: %s', post) page_id = int(post['page_id']) @@ -284,7 +284,7 @@ class WebsiteSurvey(http.Controller): # Printing routes @http.route(['/survey/print/', '/survey/print//'], - type='http', auth='public', multilang=True, website=True) + type='http', auth='public', website=True) def print_survey(self, survey, token=None, **post): '''Display an survey in printable view; if is set, it will grab the answers of the user_input_id that has .''' @@ -295,7 +295,7 @@ class WebsiteSurvey(http.Controller): 'quizz_correction': True if survey.quizz_mode and token else False}) @http.route(['/survey/results/'], - type='http', auth='user', multilang=True, website=True) + type='http', auth='user', website=True) def survey_reporting(self, survey, token=None, **post): '''Display survey Results & Statistics for given survey.''' result_template, current_filters, filter_display_data, filter_finish = 'survey.result', [], [], False @@ -310,7 +310,8 @@ class WebsiteSurvey(http.Controller): current_filters = survey_obj.filter_input_ids(request.cr, request.uid, filter_data, filter_finish, context=request.context) filter_display_data = survey_obj.get_filter_display_data(request.cr, request.uid, filter_data, context=request.context) return request.website.render(result_template, - {'survey_dict': self.prepare_result_dict(survey, current_filters), + {'survey': survey, + 'survey_dict': self.prepare_result_dict(survey, current_filters), 'page_range': self.page_range, 'current_filters': current_filters, 'filter_display_data': filter_display_data, diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 2620e4ed40f..1ea14ee8c81 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -46,6 +46,8 @@ instance.web.Notification = instance.web.Widget.extend({ } }); +var opened_modal = []; + instance.web.action_notify = function(element, action) { element.do_notify(action.params.title, action.params.text, action.params.sticky); }; @@ -113,6 +115,8 @@ instance.web.Dialog = instance.web.Widget.extend({ this.init_dialog(); } this.$buttons.insertAfter(this.$dialog_box.find(".modal-body")); + //add to list of currently opened modal + opened_modal.push(this.$dialog_box); return this; }, _add_buttons: function(buttons) { @@ -152,7 +156,7 @@ instance.web.Dialog = instance.web.Widget.extend({ 'keyboard': true, }); if (options.size !== 'large'){ - var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg') + var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg'); if (options.size === 'small'){ dialog_class_size.addClass('modal-sm'); } @@ -165,7 +169,7 @@ instance.web.Dialog = instance.web.Widget.extend({ } $dialog_content.openerpClass(); - this.$dialog_box.on('hidden.bs.modal', this, function(){ + this.$dialog_box.on('hidden.bs.modal', this, function() { self.close(); }); this.$dialog_box.modal('show'); @@ -175,13 +179,15 @@ instance.web.Dialog = instance.web.Widget.extend({ return res; }, /** - Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed. + Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead. */ close: function(reason) { - if (this.dialog_inited) { + if (this.dialog_inited && !this.__tmp_dialog_hiding) { this.trigger("closing", reason); if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal - this.$el.parents('.modal').modal('hide'); + this.__tmp_dialog_hiding = true; + this.$dialog_box.modal('hide'); + this.__tmp_dialog_hiding = undefined; } } }, @@ -212,9 +218,17 @@ instance.web.Dialog = instance.web.Widget.extend({ //we need this to put the instruction to remove modal from DOM at the end //of the queue, otherwise it might already have been removed before the modal-backdrop //is removed when pressing escape key - var $parent = this.$el.parents('.modal'); + var $element = this.$dialog_box; setTimeout(function () { - $parent.remove(); + //remove modal from list of opened modal since we just destroy it + var modal_list_index = $.inArray($element, opened_modal); + if (modal_list_index > -1){ + opened_modal.splice(modal_list_index,1)[0].remove(); + } + if (opened_modal.length > 0){ + //we still have other opened modal so we should focus it + opened_modal[opened_modal.length-1].focus(); + } },0); } this._super(); @@ -239,8 +253,7 @@ instance.web.CrashManager = instance.web.Class.extend({ this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }}); return; } - if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" - || error.data.exception_type === "access_error") { + if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") { this.show_warning(error); } else { this.show_error(error); diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 4180ecf4c69..4b951a8446f 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -1,24 +1,18 @@ # -*- coding: utf-8 -*- import cStringIO -import contextlib -import hashlib +import datetime +from itertools import islice import json import logging -import os -import datetime import re from sys import maxint -import werkzeug -import werkzeug.exceptions import werkzeug.utils import werkzeug.wrappers from PIL import Image import openerp -from openerp.osv import fields -from openerp.addons.website.models import website from openerp.addons.web import http from openerp.http import request, Response @@ -27,12 +21,13 @@ logger = logging.getLogger(__name__) # Completely arbitrary limits MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768) LOC_PER_SITEMAP = 45000 +SITEMAP_CACHE_TIME = datetime.timedelta(hours=12) class Website(openerp.addons.web.controllers.main.Home): #------------------------------------------------------ # View #------------------------------------------------------ - @http.route('/', type='http', auth="public", website=True, multilang=True) + @http.route('/', type='http', auth="public", website=True) def index(self, **kw): page = 'homepage' try: @@ -47,12 +42,12 @@ class Website(openerp.addons.web.controllers.main.Home): pass return self.page(page) - @http.route(website=True, auth="public", multilang=True) + @http.route(website=True, auth="public") def web_login(self, *args, **kw): # TODO: can't we just put auth=public, ... in web client ? return super(Website, self).web_login(*args, **kw) - @http.route('/page/', type='http', auth="public", website=True, multilang=True) + @http.route('/page/', type='http', auth="public", website=True) def page(self, page, **opt): values = { 'path': page, @@ -78,33 +73,64 @@ class Website(openerp.addons.web.controllers.main.Home): @http.route('/sitemap.xml', type='http', auth="public", website=True) def sitemap_xml_index(self): - pages = list(request.website.enumerate_pages()) - if len(pages)<=LOC_PER_SITEMAP: - return self.__sitemap_xml(pages, 0) - # Sitemaps must be split in several smaller files with a sitemap index - values = { - 'pages': range(len(pages)/LOC_PER_SITEMAP+1), - 'url_root': request.httprequest.url_root - } - headers = { - 'Content-Type': 'application/xml;charset=utf-8', - } - return request.render('website.sitemap_index_xml', values, headers=headers) + cr, uid, context = request.cr, openerp.SUPERUSER_ID, request.context + ira = request.registry['ir.attachment'] + iuv = request.registry['ir.ui.view'] + mimetype ='application/xml;charset=utf-8' + content = None - @http.route('/sitemap-.xml', type='http', auth="public", website=True) - def sitemap_xml(self, page): - pages = list(request.website.enumerate_pages()) - return self.__sitemap_xml(pages, page) + def create_sitemap(url, content): + ira.create(cr, uid, dict( + datas=content.encode('base64'), + mimetype=mimetype, + type='binary', + name=url, + url=url, + ), context=context) - def __sitemap_xml(self, pages, index=0): - values = { - 'pages': pages[index*LOC_PER_SITEMAP:(index+1)*LOC_PER_SITEMAP], - 'url_root': request.httprequest.url_root.rstrip('/') - } - headers = { - 'Content-Type': 'application/xml;charset=utf-8', - } - return request.render('website.sitemap_xml', values, headers=headers) + sitemap = ira.search_read(cr, uid, [('url', '=' , '/sitemap.xml'), ('type', '=', 'binary')], ('datas', 'create_date'), context=context) + if sitemap: + # Check if stored version is still valid + server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT + create_date = datetime.datetime.strptime(sitemap[0]['create_date'], server_format) + delta = datetime.datetime.now() - create_date + if delta < SITEMAP_CACHE_TIME: + content = sitemap[0]['datas'].decode('base64') + + if not content: + # Remove all sitemaps in ir.attachments as we're going to regenerated them + sitemap_ids = ira.search(cr, uid, [('url', '=like' , '/sitemap%.xml'), ('type', '=', 'binary')], context=context) + if sitemap_ids: + ira.unlink(cr, uid, sitemap_ids, context=context) + + pages = 0 + first_page = None + locs = request.website.enumerate_pages() + while True: + start = pages * LOC_PER_SITEMAP + loc_slice = islice(locs, start, start + LOC_PER_SITEMAP) + urls = iuv.render(cr, uid, 'website.sitemap_locs', dict(locs=loc_slice), context=context) + if urls.strip(): + page = iuv.render(cr, uid, 'website.sitemap_xml', dict(content=urls), context=context) + if not first_page: + first_page = page + pages += 1 + create_sitemap('/sitemap-%d.xml' % pages, page) + else: + break + if not pages: + return request.not_found() + elif pages == 1: + content = first_page + else: + # Sitemaps must be split in several smaller files with a sitemap index + content = iuv.render(cr, uid, 'website.sitemap_index_xml', dict( + pages=range(1, pages + 1), + url_root=request.httprequest.url_root, + ), context=context) + create_sitemap('/sitemap.xml', content) + + return request.make_response(content, [('Content-Type', mimetype)]) #------------------------------------------------------ # Edit @@ -332,13 +358,7 @@ class Website(openerp.addons.web.controllers.main.Home): return request.website.kanban_col(**post) def placeholder(self, response): - # file_open may return a StringIO. StringIO can be closed but are - # not context managers in Python 2 though that is fixed in 3 - with contextlib.closing(openerp.tools.misc.file_open( - os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), - mode='rb')) as f: - response.data = f.read() - return response.make_conditional(request.httprequest) + return request.registry['website']._image_placeholder(response) @http.route([ '/website/image', @@ -354,60 +374,15 @@ class Website(openerp.addons.web.controllers.main.Home): Sets and checks conditional response parameters: * :mailheader:`ETag` is always set (and checked) * :mailheader:`Last-Modified is set iif the record has a concurrency - field (``write_date``) + field (``__last_update``) The requested field is assumed to be base64-encoded image data in all cases. """ - id = int(id) response = werkzeug.wrappers.Response() - concurrency = 'write_date' - try: - [record] = request.registry[model].read(request.cr, openerp.SUPERUSER_ID, [id], - [concurrency, field], - context=request.context) - except: - return self.placeholder(response) + return request.registry['website']._image( + request.cr, request.uid, model, id, field, response) - if concurrency in record: - server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT - try: - response.last_modified = datetime.datetime.strptime( - record[concurrency], server_format + '.%f') - except ValueError: - # just in case we have a timestamp without microseconds - response.last_modified = datetime.datetime.strptime( - record[concurrency], server_format) - - # Field does not exist on model or field set to False - if not record.get(field): - # FIXME: maybe a field which does not exist should be a 404? - return self.placeholder(response) - - response.set_etag(hashlib.sha1(record[field]).hexdigest()) - response.make_conditional(request.httprequest) - - # conditional request match - if response.status_code == 304: - return response - - data = record[field].decode('base64') - if (not max_width) and (not max_height): - response.data = data - return response - - image = Image.open(cStringIO.StringIO(data)) - response.mimetype = Image.MIME[image.format] - - w, h = image.size - max_w, max_h = int(max_width), int(max_height) - if w < max_w and h < max_h: - response.data = data - else: - image.thumbnail((max_w, max_h), Image.ANTIALIAS) - image.save(response.stream, image.format) - del response.headers['Content-Length'] - return response #------------------------------------------------------ # Server actions diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index d2b5c437ea6..57e7d284922 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -51,6 +51,8 @@ class ir_http(orm.AbstractModel): # in all cases, website processes them request.website_enabled = True + request.website_multilang = request.website_enabled and func and func.routing.get('multilang', True) + if request.website_enabled: if func: self._authenticate(func.routing['auth']) @@ -67,6 +69,10 @@ class ir_http(orm.AbstractModel): if path[1] in langs: request.lang = request.context['lang'] = path.pop(1) path = '/'.join(path) or '/' + if request.lang == request.website.default_lang_code: + # If language is in the url and it is the default language, redirect + # to url without language so google doesn't see duplicate content + return request.redirect(path + request.httprequest.query_string) return self.reroute(path) return self._handle_exception(code=404) return super(ir_http, self)._dispatch() diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 5e339a21d03..433d8644698 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- +import cStringIO +import contextlib +import datetime import hashlib import inspect import itertools import logging import math import mimetypes +import os import re import urlparse +from PIL import Image +from sys import maxint + import werkzeug import werkzeug.exceptions import werkzeug.utils @@ -72,7 +79,7 @@ def is_multilang_url(local_url, langs=None): query_string = url[1] if len(url) > 1 else None router = request.httprequest.app.get_db_router(request.db).bind('') func = router.match(path, query_args=query_string)[0] - return func.routing.get('multilang', False) + return func.routing.get('website', False) and func.routing.get('multilang', True) except Exception: return False @@ -459,6 +466,95 @@ class website(osv.osv): html += request.website._render(template, {'object_id': object_id}) return html + def _image_placeholder(self, response): + # file_open may return a StringIO. StringIO can be closed but are + # not context managers in Python 2 though that is fixed in 3 + with contextlib.closing(openerp.tools.misc.file_open( + os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), + mode='rb')) as f: + response.data = f.read() + return response.make_conditional(request.httprequest) + + def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, context=None): + """ Fetches the requested field and ensures it does not go above + (max_width, max_height), resizing it if necessary. + + Resizing is bypassed if the object provides a $field_big, which will + be interpreted as a pre-resized version of the base field. + + If the record is not found or does not have the requested field, + returns a placeholder image via :meth:`~._image_placeholder`. + + Sets and checks conditional response parameters: + * :mailheader:`ETag` is always set (and checked) + * :mailheader:`Last-Modified is set iif the record has a concurrency + field (``__last_update``) + + The requested field is assumed to be base64-encoded image data in + all cases. + """ + Model = self.pool[model] + id = int(id) + + ids = Model.search(cr, uid, + [('id', '=', id)], context=context) + if not ids and 'website_published' in Model._all_columns: + ids = Model.search(cr, openerp.SUPERUSER_ID, + [('id', '=', id), ('website_published', '=', True)], context=context) + if not ids: + return self._image_placeholder(response) + + concurrency = '__last_update' + [record] = Model.read(cr, openerp.SUPERUSER_ID, [id], + [concurrency, field], + context=context) + + if concurrency in record: + server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT + try: + response.last_modified = datetime.datetime.strptime( + record[concurrency], server_format + '.%f') + except ValueError: + # just in case we have a timestamp without microseconds + response.last_modified = datetime.datetime.strptime( + record[concurrency], server_format) + + # Field does not exist on model or field set to False + if not record.get(field): + # FIXME: maybe a field which does not exist should be a 404? + return self._image_placeholder(response) + + response.set_etag(hashlib.sha1(record[field]).hexdigest()) + response.make_conditional(request.httprequest) + + # conditional request match + if response.status_code == 304: + return response + + data = record[field].decode('base64') + + if (not max_width) and (not max_height): + response.data = data + return response + + image = Image.open(cStringIO.StringIO(data)) + response.mimetype = Image.MIME[image.format] + + w, h = image.size + max_w, max_h = int(max_width), int(max_height) + + if w < max_w and h < max_h: + response.data = data + else: + image.thumbnail((max_w, max_h), Image.ANTIALIAS) + image.save(response.stream, image.format) + # invalidate content-length computed by make_conditional as + # writing to response.stream does not do it (as of werkzeug 0.9.3) + del response.headers['Content-Length'] + + return response + + class website_menu(osv.osv): _name = "website.menu" _description = "Website Menu" diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 94de70dc7dc..209d431ae8f 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -121,7 +121,7 @@ header a.navbar-brand img { /* ----- EDITOR ----- */ .css_non_editable_mode_hidden { - display: none; + display: none !important; } /* ----- BOOTSTRAP FIX ----- */ diff --git a/addons/website/tests/test_requests.py b/addons/website/tests/test_requests.py index 2d14cbc668d..2a1eea4c877 100644 --- a/addons/website/tests/test_requests.py +++ b/addons/website/tests/test_requests.py @@ -41,6 +41,9 @@ class CrawlSuite(unittest2.TestSuite): starting the crawl """ + at_install = False + post_install = True + def __init__(self, user=None, password=None): super(CrawlSuite, self).__init__() @@ -108,14 +111,23 @@ class CrawlSuite(unittest2.TestSuite): for link in doc.xpath('//a[@href]'): href = link.get('href') + parts = urlparse.urlsplit(href) + # href with any fragment removed + href = urlparse.urlunsplit(( + parts.scheme, + parts.netloc, + parts.path, + parts.query, + '' + )) + # avoid repeats, even for links we won't crawl no need to # bother splitting them if we've already ignored them # previously if href in seen: continue seen.add(href) - parts = urlparse.urlsplit(href) - + # FIXME: handle relative link (not parts.path.startswith /) if parts.netloc or \ not parts.path.startswith('/') or \ parts.path == '/web' or\ diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index c62a31a95e1..decc6d5618b 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -15,8 +15,6 @@ - - @@ -82,10 +80,12 @@ and main_object.website_meta_keywords or website_meta_keywords"/> <t t-esc="title"/> - - - - + + + + + + @@ -177,7 +177,7 @@ their performance.

-
    +
    • @@ -709,14 +709,18 @@ User-agent: * Sitemap: sitemap.xml + + diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index e33905272a3..a36e9fcfafe 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -51,7 +51,7 @@ class WebsiteBlog(http.Controller): blog_post_obj = request.registry['blog.post'] groups = blog_post_obj.read_group( request.cr, request.uid, [], ['name', 'create_date'], - groupby="create_date", orderby="create_date asc", context=request.context) + groupby="create_date", orderby="create_date desc", context=request.context) for group in groups: begin_date = datetime.datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() end_date = datetime.datetime.strptime(group['__domain'][1][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() @@ -62,7 +62,7 @@ class WebsiteBlog(http.Controller): @http.route([ '/blog', '/blog/page/', - ], type='http', auth="public", website=True, multilang=True) + ], type='http', auth="public", website=True) def blogs(self, page=1, **post): cr, uid, context = request.cr, request.uid, request.context blog_obj = request.registry['blog.post'] @@ -87,7 +87,7 @@ class WebsiteBlog(http.Controller): '/blog//page/', '/blog//tag/', '/blog//tag//page/', - ], type='http', auth="public", website=True, multilang=True) + ], type='http', auth="public", website=True) def blog(self, blog=None, tag=None, page=1, **opt): """ Prepare all values to display the blog. @@ -122,7 +122,7 @@ class WebsiteBlog(http.Controller): blog_url = QueryURL('', ['blog', 'tag'], blog=blog, tag=tag, date_begin=date_begin, date_end=date_end) post_url = QueryURL('', ['blogpost'], tag_id=tag and tag.id or None, date_begin=date_begin, date_end=date_end) - blog_post_ids = blog_post_obj.search(cr, uid, domain, order="create_date asc", context=context) + blog_post_ids = blog_post_obj.search(cr, uid, domain, order="create_date desc", context=context) blog_posts = blog_post_obj.browse(cr, uid, blog_post_ids, context=context) pager = request.website.pager( @@ -156,7 +156,7 @@ class WebsiteBlog(http.Controller): @http.route([ '''/blog//post/

      - + &nbsp; @@ -187,6 +187,7 @@

      +
      diff --git a/addons/website_certification/controllers/main.py b/addons/website_certification/controllers/main.py index ab955bc12dd..303af758a09 100644 --- a/addons/website_certification/controllers/main.py +++ b/addons/website_certification/controllers/main.py @@ -28,7 +28,7 @@ class WebsiteCertifiedPartners(http.Controller): @http.route(['/certifications', '/certifications/'], type='http', auth='public', - website=True, multilang=True) + website=True) def certified_partners(self, cert_type=None, **post): cr, uid, context = request.cr, request.uid, request.context certification_obj = request.registry['certification.certification'] diff --git a/addons/website_crm/controllers/main.py b/addons/website_crm/controllers/main.py index 163e3ee9793..fb4535bf3a6 100644 --- a/addons/website_crm/controllers/main.py +++ b/addons/website_crm/controllers/main.py @@ -16,45 +16,36 @@ class contactus(http.Controller): ) return url - @http.route(['/page/website.contactus'], type='http', auth="public", website=True, multilang=True) + @http.route(['/page/website.contactus', '/page/contactus'], type='http', auth="public", website=True) def contact(self, **kwargs): values = {} for field in ['description', 'partner_name', 'phone', 'contact_name', 'email_from', 'name']: if kwargs.get(field): values[field] = kwargs.pop(field) values.update(kwargs=kwargs.items()) - print values return request.website.render("website.contactus", values) - @http.route(['/crm/contactus'], type='http', auth="public", website=True, multilang=True) + @http.route(['/crm/contactus'], type='http', auth="public", website=True) def contactus(self, description=None, partner_name=None, phone=None, contact_name=None, email_from=None, name=None, **kwargs): - post = {} - post['description'] = description - post['partner_name'] = partner_name - post['phone'] = phone - post['contact_name'] = contact_name - post['email_from'] = email_from - post['name'] = name - - required_fields = ['contact_name', 'email_from', 'description'] - error = set() - values = dict((key, post.get(key)) for key in post) - values['error'] = error + post = { + 'description': description, + 'partner_name': partner_name, + 'phone': phone, + 'contact_name': contact_name, + 'email_from': email_from, + 'name': name or contact_name, + 'user_id': False, + } # fields validation - for field in required_fields: - if not post.get(field): - error.add(field) + error = set(field for field in ['contact_name', 'email_from', 'description'] + if not post.get(field)) + + values = dict(post, error=error) if error: values.update(kwargs=kwargs.items()) return request.website.render("website.contactus", values) - # if not given: subject is contact name - if not post.get('name'): - post['name'] = post.get('contact_name') - - post['user_id'] = False - try: post['channel_id'] = request.registry['ir.model.data'].get_object_reference(request.cr, SUPERUSER_ID, 'crm', 'crm_case_channel_website')[1] except ValueError: @@ -71,6 +62,7 @@ class contactus(http.Controller): if not hasattr(field_value, 'filename'): post['description'] = "%s\n%s: %s" % (post['description'], field_name, field_value) + post['section_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'website.salesteam_website_sales') lead_id = request.registry['crm.lead'].create(request.cr, SUPERUSER_ID, post, request.context) for field_name, field_value in kwargs.items(): diff --git a/addons/website_crm/data/website_crm_data.xml b/addons/website_crm/data/website_crm_data.xml index 80c8530935d..ba131d22f34 100644 --- a/addons/website_crm/data/website_crm_data.xml +++ b/addons/website_crm/data/website_crm_data.xml @@ -12,11 +12,12 @@ open - - Website - Website - True + + Website Sales + WS + True + diff --git a/addons/website_crm_partner_assign/__openerp__.py b/addons/website_crm_partner_assign/__openerp__.py index 82b16173666..06b6dc9410e 100644 --- a/addons/website_crm_partner_assign/__openerp__.py +++ b/addons/website_crm_partner_assign/__openerp__.py @@ -10,6 +10,7 @@ Publish and Assign Partner 'author': 'OpenERP SA', 'depends': ['crm_partner_assign','website_partner', 'website_google_map'], 'data': [ + 'views/partner_grade.xml', 'views/website_crm_partner_assign.xml', ], 'qweb': ['static/src/xml/*.xml'], diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 4f5639237f4..c23bddc5ea2 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -1,121 +1,172 @@ # -*- coding: utf-8 -*- - +import logging +import re import werkzeug -import openerp +_logger = logging.getLogger(__name__) +try: + import GeoIP +except ImportError: + GeoIP = None + _logger.warn("Please install GeoIP python module to use events localisation.") + from openerp import SUPERUSER_ID from openerp.addons.web import http -from openerp.tools.translate import _ from openerp.addons.web.http import request -from openerp.addons.website_partner.controllers import main as website_partner +from openerp.addons.website.models.website import slug +from openerp.tools.translate import _ + class WebsiteCrmPartnerAssign(http.Controller): - _references_per_page = 20 + _references_per_page = 40 + + def _get_current_country_code(self): + if not GeoIP: + return False + GI = GeoIP.open('/usr/share/GeoIP/GeoIP.dat', 0) + return GI.country_code_by_addr(request.httprequest.remote_addr) @http.route([ '/partners', '/partners/page/', - '/partners/grade/', - '/partners/grade//page/', + '/partners/grade/', + '/partners/grade//page/', - '/partners/country/', - '/partners/country/-', - '/partners/country//page/', - '/partners/country/-/page/', - - '/partners/grade//country/', - '/partners/grade//country/-', - '/partners/grade//country//page/', - '/partners/grade//country/-/page/', - - ], type='http', auth="public", website=True, multilang=True) - def partners(self, country_id=0, grade_id=0, page=0, **post): - country_obj = request.registry['res.country'] + '/partners/country/', + '/partners/country//page/', + + '/partners/grade//country/', + '/partners/grade//country//page/', + ], type='http', auth="public", website=True) + def partners(self, country=None, grade=None, page=0, **post): + country_all = post.pop('country_all', False) partner_obj = request.registry['res.partner'] - post_name = post.get('search', '') - country = None + country_obj = request.registry['res.country'] + search = post.get('search', '') - # format displayed membership lines domain - base_partner_domain = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True)] - partner_domain = list(base_partner_domain) - if post_name: - partner_domain += ['|', ('name', 'ilike', post_name), ('website_description', 'ilike', post_name)] - if grade_id and grade_id != "all": - partner_domain += [('grade_id', '=', int(grade_id))] # try/catch int - - # group by country - countries = partner_obj.read_group( - request.cr, openerp.SUPERUSER_ID, partner_domain, ["id", "country_id"], - groupby="country_id", orderby="country_id", context=request.context) - countries_partners = partner_obj.search( - request.cr, openerp.SUPERUSER_ID, partner_domain, - context=request.context, count=True) - - if country_id: - country = country_obj.browse(request.cr, request.uid, country_id, request.context) - partner_domain += [('country_id', '=', country_id)] - if not any(x['country_id'][0] == country_id for x in countries): - countries.append({ - 'country_id_count': 0, - 'country_id': (country_id, country.name) - }) - countries.sort(key=lambda d: d['country_id'][1]) - - countries.insert(0, { - 'country_id_count': countries_partners, - 'country_id': (0, _("All Countries")) - }) - - - # format pager - partner_ids = partner_obj.search( - request.cr, openerp.SUPERUSER_ID, partner_domain, - context=request.context) - pager = request.website.pager(url="/partners", total=len(partner_ids), page=page, step=self._references_per_page, scope=7, url_args=post) - - # search for partners to display - partners_data = partner_obj.search_read(request.cr, openerp.SUPERUSER_ID, - domain=partner_domain, - fields=request.website.get_partner_white_list_fields(), - offset=pager['offset'], - limit=self._references_per_page, - order="grade_id DESC,partner_weight DESC", - context=request.context) - google_map_partner_ids = ",".join([str(p['id']) for p in partners_data]) + base_partner_domain = [('is_company', '=', True), ('grade_id.website_published', '=', True), ('website_published', '=', True)] + if search: + base_partner_domain += ['|', ('name', 'ilike', search), ('website_description', 'ilike', search)] # group by grade + grade_domain = list(base_partner_domain) + if not country and not country_all: + country_code = self._get_current_country_code() + if country_code: + country_ids = country_obj.search(request.cr, request.uid, [('code', '=', country_code)], context=request.context) + if country_ids: + country = country_obj.browse(request.cr, request.uid, country_ids[0], context=request.context) + if country: + grade_domain += [('country_id', '=', country.id)] grades = partner_obj.read_group( - request.cr, openerp.SUPERUSER_ID, base_partner_domain, ["id", "grade_id"], - groupby="grade_id", orderby="grade_id", context=request.context) + request.cr, SUPERUSER_ID, grade_domain, ["id", "grade_id"], + groupby="grade_id", orderby="grade_id DESC", context=request.context) grades_partners = partner_obj.search( - request.cr, openerp.SUPERUSER_ID, base_partner_domain, + request.cr, SUPERUSER_ID, grade_domain, context=request.context, count=True) + # flag active grade + for grade_dict in grades: + grade_dict['active'] = grade and grade_dict['grade_id'][0] == grade.id grades.insert(0, { 'grade_id_count': grades_partners, - 'grade_id': (0, _("All Categories")) + 'grade_id': (0, _("All Categories")), + 'active': bool(grade is None), }) + # group by country + country_domain = list(base_partner_domain) + if grade: + country_domain += [('grade_id', '=', grade.id)] + countries = partner_obj.read_group( + request.cr, SUPERUSER_ID, country_domain, ["id", "country_id"], + groupby="country_id", orderby="country_id", context=request.context) + countries_partners = partner_obj.search( + request.cr, SUPERUSER_ID, country_domain, + context=request.context, count=True) + # flag active country + for country_dict in countries: + country_dict['active'] = country and country_dict['country_id'] and country_dict['country_id'][0] == country.id + countries.insert(0, { + 'country_id_count': countries_partners, + 'country_id': (0, _("All Countries")), + 'active': bool(country is None), + }) + + # current search + if grade: + base_partner_domain += [('grade_id', '=', grade.id)] + if country: + base_partner_domain += [('country_id', '=', country.id)] + + # format pager + if grade and not country: + url = '/partners/grade/' + slug(grade) + elif country and not grade: + url = '/partners/country/' + slug(country) + elif country and grade: + url = '/partners/grade/' + slug(grade) + '/country/' + slug(country) + else: + url = '/partners' + url_args = {} + if search: + url_args['search'] = search + partner_count = partner_obj.search_count( + request.cr, SUPERUSER_ID, base_partner_domain, + context=request.context) + pager = request.website.pager( + url=url, total=partner_count, page=page, step=self._references_per_page, scope=7, + url_args=url_args) + + # search partners matching current search parameters + partner_ids = partner_obj.search( + request.cr, SUPERUSER_ID, base_partner_domain, + order="grade_id DESC", + context=request.context) # todo in trunk: order="grade_id DESC, implemented_count DESC", offset=pager['offset'], limit=self._references_per_page + partners = partner_obj.browse(request.cr, SUPERUSER_ID, partner_ids, request.context) + # remove me in trunk + partners.sort(key=lambda x: (-1 * (x.grade_id and x.grade_id.id or 0), len(x.implemented_partner_ids)), reverse=True) + partners = partners[pager['offset']:pager['offset'] + self._references_per_page] + + google_map_partner_ids = ','.join(map(str, [p.id for p in partners])) + values = { 'countries': countries, - 'current_country_id': country_id, 'current_country': country, 'grades': grades, - 'grade_id': grade_id, - 'partners_data': partners_data, + 'current_grade': grade, + 'partners': partners, 'google_map_partner_ids': google_map_partner_ids, 'pager': pager, 'searches': post, - 'search_path': "?%s" % werkzeug.url_encode(post), + 'search_path': "%s" % werkzeug.url_encode(post), } return request.website.render("website_crm_partner_assign.index", values) # Do not use semantic controller due to SUPERUSER_ID - @http.route(['/partners/', '/partners/-'], type='http', auth="public", website=True, multilang=True) - def partners_ref(self, partner_id, **post): - partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) - values = website_partner.get_partner_template_value(partner) - if not values: - return self.partners(**post) - values['main_object'] = values['partner'] - return request.website.render("website_crm_partner_assign.partner", values) + @http.route(['/partners/'], type='http', auth="public", website=True) + def partners_detail(self, partner_id, partner_name='', **post): + mo = re.search('-([-0-9]+)$', str(partner_id)) + current_grade, current_country = None, None + grade_id = post.get('grade_id') + country_id = post.get('country_id') + if grade_id: + grade_ids = request.registry['res.partner.grade'].exists(request.cr, request.uid, int(grade_id), context=request.context) + if grade_ids: + current_grade = request.registry['res.partner.grade'].browse(request.cr, request.uid, grade_ids[0], context=request.context) + if country_id: + country_ids = request.registry['res.country'].exists(request.cr, request.uid, int(country_id), context=request.context) + if country_ids: + current_country = request.registry['res.country'].browse(request.cr, request.uid, country_ids[0], context=request.context) + if mo: + partner_id = int(mo.group(1)) + partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) + if partner.exists() and partner.website_published: + values = { + 'main_object': partner, + 'partner': partner, + 'current_grade': current_grade, + 'current_country': current_country + } + return request.website.render("website_crm_partner_assign.partner", values) + return self.partners(**post) diff --git a/addons/website_crm_partner_assign/models/__init__.py b/addons/website_crm_partner_assign/models/__init__.py index 20036e92f6c..4e5bc1614d1 100644 --- a/addons/website_crm_partner_assign/models/__init__.py +++ b/addons/website_crm_partner_assign/models/__init__.py @@ -1,2 +1 @@ import res_partner -import website diff --git a/addons/website_crm_partner_assign/models/res_partner.py b/addons/website_crm_partner_assign/models/res_partner.py index e295c7ab0b1..cc00f8bd6e1 100644 --- a/addons/website_crm_partner_assign/models/res_partner.py +++ b/addons/website_crm_partner_assign/models/res_partner.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- - from openerp.osv import osv, fields class res_partner_grade(osv.osv): _inherit = 'res.partner.grade' _columns = { - 'website_description': fields.html('Description for the website'), + 'website_published': fields.boolean('Published On Website'), } diff --git a/addons/website_crm_partner_assign/models/website.py b/addons/website_crm_partner_assign/models/website.py deleted file mode 100644 index f0d428ae5fa..00000000000 --- a/addons/website_crm_partner_assign/models/website.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -from openerp.osv import orm - - -class Website(orm.Model): - _inherit = 'website' - - def get_partner_white_list_fields(self, cr, uid, ids, context=None): - fields = super(Website, self).get_partner_white_list_fields(cr, uid, ids, context=context) - fields += ["grade_id"] - return fields diff --git a/addons/website_crm_partner_assign/views/partner_grade.xml b/addons/website_crm_partner_assign/views/partner_grade.xml new file mode 100644 index 00000000000..2acf8e1eb24 --- /dev/null +++ b/addons/website_crm_partner_assign/views/partner_grade.xml @@ -0,0 +1,18 @@ + + + + + + res.partner.grade.website + res.partner.grade + + + + + + + + + + + diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml index 56ac22d1eee..6d5b93793ad 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign.xml @@ -15,7 +15,7 @@ Resellers
      -
      +
      @@ -36,13 +36,13 @@
      -
      +
      -
      -
      +