diff options
| author | Christophe Simonis <chs@openerp.com> | 2014-02-20 17:38:15 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@openerp.com> | 2014-02-20 17:38:15 +0100 |
| commit | 4b72678bc74906e552ebc8cace7161901571d2c1 (patch) | |
| tree | d4965fd90f45af167c6a34eec3caad111a309f12 | |
| parent | 65db3c635eea5324676c8f85d13e0058d66ce1b9 (diff) | |
| parent | d5106a4d34a9b7c52a1ecad7c65331f1fda2e231 (diff) | |
[MERGE] forward port of branch saas-2 up to revid 9195 chs@openerp.com-20140220154517-6y531n1mqtnfnsmr
bzr revid: chs@openerp.com-20140220163815-69sza1kclnh5mavs
100 files changed, 3089 insertions, 1018 deletions
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 42f3effd6f9..06e59232ac9 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -114,7 +114,7 @@ class account_invoice(osv.osv): #we check if the invoice is partially reconciled and if there are other invoices #involved in this partial reconciliation (and we sum these invoices) for line in aml.reconcile_partial_id.line_partial_ids: - if line.invoice: + if line.invoice and invoice.type == line.invoice.type: nb_inv_in_partial_rec += 1 #store the max invoice id as for this invoice we will make a balance instead of a simple division max_invoice_id = max(max_invoice_id, line.invoice.id) diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 0ef39d5884d..48d6320fbc3 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -1451,7 +1451,7 @@ <act_window id="act_account_move_to_account_move_line_open" name="Journal Items" - context="{'search_default_journal_id': active_id, 'default_journal_id': active_id}" + context="{'search_default_move_id': active_id, 'default_move_id': active_id}" res_model="account.move.line" src_model="account.move"/> diff --git a/addons/account/static/src/js/account_move_line_quickadd.js b/addons/account/static/src/js/account_move_line_quickadd.js index 997b2b03bb6..0bbdb959d17 100644 --- a/addons/account/static/src/js/account_move_line_quickadd.js +++ b/addons/account/static/src/js/account_move_line_quickadd.js @@ -22,6 +22,7 @@ openerp.account.quickadd = function (instance) { start:function(){ var tmp = this._super.apply(this, arguments); var self = this; + var defs = []; this.$el.parent().prepend(QWeb.render("AccountMoveLineQuickAdd", {widget: this})); this.$el.parent().find('.oe_account_select_journal').change(function() { @@ -41,11 +42,17 @@ openerp.account.quickadd = function (instance) { self.$el.parent().find('.oe_account_select_period').removeAttr('disabled'); }); var mod = new instance.web.Model("account.move.line", self.dataset.context, self.dataset.domain); - mod.call("default_get", [['journal_id','period_id'],self.dataset.context]).then(function(result) { + defs.push(mod.call("default_get", [['journal_id','period_id'],self.dataset.context]).then(function(result) { self.current_period = result['period_id']; self.current_journal = result['journal_id']; - }); - return tmp; + })); + defs.push(mod.call("list_journals", []).then(function(result) { + self.journals = result; + })); + defs.push(mod.call("list_periods", []).then(function(result) { + self.periods = result; + })); + return $.when(tmp, defs); }, do_search: function(domain, context, group_by) { var self = this; @@ -53,34 +60,27 @@ openerp.account.quickadd = function (instance) { this.last_context = context; this.last_group_by = group_by; this.old_search = _.bind(this._super, this); - var mod = new instance.web.Model("account.move.line", context, domain); - return $.when(mod.call("list_journals", []).then(function(result) { - self.journals = result; - }),mod.call("list_periods", []).then(function(result) { - self.periods = result; - })).then(function () { - var o; - self.$el.parent().find('.oe_account_select_journal').children().remove().end(); - self.$el.parent().find('.oe_account_select_journal').append(new Option('', '')); - for (var i = 0;i < self.journals.length;i++){ - o = new Option(self.journals[i][1], self.journals[i][0]); - if (self.journals[i][0] === self.current_journal){ - self.current_journal_type = self.journals[i][2]; - self.current_journal_currency = self.journals[i][3]; - self.current_journal_analytic = self.journals[i][4]; - $(o).attr('selected',true); - } - self.$el.parent().find('.oe_account_select_journal').append(o); + var o; + self.$el.parent().find('.oe_account_select_journal').children().remove().end(); + self.$el.parent().find('.oe_account_select_journal').append(new Option('', '')); + for (var i = 0;i < self.journals.length;i++){ + o = new Option(self.journals[i][1], self.journals[i][0]); + if (self.journals[i][0] === self.current_journal){ + self.current_journal_type = self.journals[i][2]; + self.current_journal_currency = self.journals[i][3]; + self.current_journal_analytic = self.journals[i][4]; + $(o).attr('selected',true); } - self.$el.parent().find('.oe_account_select_period').children().remove().end(); - self.$el.parent().find('.oe_account_select_period').append(new Option('', '')); - for (var i = 0;i < self.periods.length;i++){ - o = new Option(self.periods[i][1], self.periods[i][0]); - self.$el.parent().find('.oe_account_select_period').append(o); - } - self.$el.parent().find('.oe_account_select_period').val(self.current_period).attr('selected',true); - return self.search_by_journal_period(); - }); + self.$el.parent().find('.oe_account_select_journal').append(o); + } + self.$el.parent().find('.oe_account_select_period').children().remove().end(); + self.$el.parent().find('.oe_account_select_period').append(new Option('', '')); + for (var i = 0;i < self.periods.length;i++){ + o = new Option(self.periods[i][1], self.periods[i][0]); + self.$el.parent().find('.oe_account_select_period').append(o); + } + self.$el.parent().find('.oe_account_select_period').val(self.current_period).attr('selected',true); + return self.search_by_journal_period(); }, search_by_journal_period: function() { var self = this; @@ -93,7 +93,9 @@ openerp.account.quickadd = function (instance) { self.last_context["journal_type"] = self.current_journal_type; self.last_context["currency"] = self.current_journal_currency; self.last_context["analytic_journal_id"] = self.current_journal_analytic; - return self.old_search(new instance.web.CompoundDomain(self.last_domain, domain), self.last_context, self.last_group_by); + var compound_domain = new instance.web.CompoundDomain(self.last_domain, domain); + self.dataset.domain = compound_domain.eval(); + return self.old_search(compound_domain, self.last_context, self.last_group_by); }, }); }; diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index faf4e29c93c..ec5e2c56e6c 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -664,13 +664,20 @@ class account_analytic_account(osv.osv): partner_payment_term = contract.partner_id.property_payment_term and contract.partner_id.property_payment_term.id or False + currency_id = False + if contract.pricelist_id: + currency_id = contract.pricelist_id.currency_id.id + elif contract.partner_id.property_product_pricelist: + currency_id = contract.partner_id.property_product_pricelist.currency_id.id + elif contract.company_id: + currency_id = contract.company_id.currency_id.id inv_data = { 'reference': contract.code or False, 'account_id': contract.partner_id.property_account_receivable.id, 'type': 'out_invoice', 'partner_id': contract.partner_id.id, - 'currency_id': contract.partner_id.property_product_pricelist.id or False, + 'currency_id': currency_id, 'journal_id': len(journal_ids) and journal_ids[0] or False, 'date_invoice': contract.recurring_next_date, 'origin': contract.name, diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index c9516b6135f..5d0ed8297c2 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -911,9 +911,10 @@ class account_voucher(osv.osv): if context.get('payment_expected_currency') and currency_id != context.get('payment_expected_currency'): vals['value']['amount'] = 0 amount = 0 - res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context) - for key in res.keys(): - vals[key].update(res[key]) + if partner_id: + res = self.onchange_partner_id(cr, uid, ids, partner_id, journal_id, amount, currency_id, ttype, date, context) + for key in res.keys(): + vals[key].update(res[key]) return vals def button_proforma_voucher(self, cr, uid, ids, context=None): @@ -965,7 +966,7 @@ class account_voucher(osv.osv): res = {} if not partner_id: return res - res = {'account_id':False} + res = {} partner_pool = self.pool.get('res.partner') journal_pool = self.pool.get('account.journal') if pay_now == 'pay_later': @@ -977,7 +978,8 @@ class account_voucher(osv.osv): account_id = partner.property_account_payable.id else: account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id - res['account_id'] = account_id + if account_id: + res['account_id'] = account_id return {'value':res} def _sel_context(self, cr, uid, voucher_id, context=None): @@ -1366,6 +1368,7 @@ class account_voucher(osv.osv): move_pool = self.pool.get('account.move') move_line_pool = self.pool.get('account.move.line') for voucher in self.browse(cr, uid, ids, context=context): + local_context = dict(context, force_company=voucher.journal_id.company_id.id) if voucher.move_id: continue company_currency = self._get_company_currency(cr, uid, voucher.id, context) @@ -1380,7 +1383,7 @@ class account_voucher(osv.osv): # Get the name of the account_move just created name = move_pool.browse(cr, uid, move_id, context=context).name # Create the first line of the voucher - move_line_id = move_line_pool.create(cr, uid, self.first_move_line_get(cr,uid,voucher.id, move_id, company_currency, current_currency, context), context) + move_line_id = move_line_pool.create(cr, uid, self.first_move_line_get(cr,uid,voucher.id, move_id, company_currency, current_currency, local_context), local_context) move_line_brw = move_line_pool.browse(cr, uid, move_line_id, context=context) line_total = move_line_brw.debit - move_line_brw.credit rec_list_ids = [] @@ -1392,9 +1395,9 @@ class account_voucher(osv.osv): line_total, rec_list_ids = self.voucher_move_line_create(cr, uid, voucher.id, line_total, move_id, company_currency, current_currency, context) # Create the writeoff line if needed - ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, context) + ml_writeoff = self.writeoff_move_line_get(cr, uid, voucher.id, line_total, move_id, name, company_currency, current_currency, local_context) if ml_writeoff: - move_line_pool.create(cr, uid, ml_writeoff, context) + move_line_pool.create(cr, uid, ml_writeoff, local_context) # We post the voucher. self.write(cr, uid, [voucher.id], { 'move_id': move_id, @@ -1605,7 +1608,11 @@ class account_bank_statement(osv.osv): bank_st_line_obj = self.pool.get('account.bank.statement.line') st_line = bank_st_line_obj.browse(cr, uid, st_line_id, context=context) if st_line.voucher_id: - voucher_obj.write(cr, uid, [st_line.voucher_id.id], {'number': next_number}, context=context) + voucher_obj.write(cr, uid, [st_line.voucher_id.id], + {'number': next_number, + 'date': st_line.date, + 'period_id': st_line.statement_id.period_id.id}, + context=context) if st_line.voucher_id.state == 'cancel': voucher_obj.action_cancel_draft(cr, uid, [st_line.voucher_id.id], context=context) voucher_obj.signal_proforma_voucher(cr, uid, [st_line.voucher_id.id]) diff --git a/addons/account_voucher/voucher_sales_purchase_view.xml b/addons/account_voucher/voucher_sales_purchase_view.xml index e35061e1836..97f96f75901 100644 --- a/addons/account_voucher/voucher_sales_purchase_view.xml +++ b/addons/account_voucher/voucher_sales_purchase_view.xml @@ -73,8 +73,14 @@ <group> <group> <field name="type" invisible="True"/> - <field name="partner_id" domain="[('customer','=',True)]" on_change="onchange_partner_id(partner_id, journal_id, amount, currency_id, type, date, context)" string="Customer" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/> <field name="company_id" widget="selection" groups="base.group_multi_company"/> + <field name="partner_id" domain="[('customer','=',True)]" on_change="onchange_partner_id(partner_id, journal_id, amount, currency_id, type, date, context)" string="Customer" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/> + <field name="account_id" + domain="[('type','in', ['liquidity', 'receivable'])]"/> + <field name="pay_now" on_change="onchange_payment(pay_now, journal_id, partner_id)" required="1"/> + <field name="date_due" attrs="{'invisible':[('pay_now','=','pay_now')]}"/> + <field name="reference" + attrs="{'invisible':[('pay_now','!=','pay_now')]}"/> </group> <group> <field name="journal_id" domain="[('type','in',['sale','sale_refund'])]" widget="selection" on_change="onchange_journal(journal_id, line_cr_ids, tax_id, partner_id, date, amount, type, company_id, context)" groups="account.group_account_user"/> @@ -112,15 +118,6 @@ </div> <field name="amount" class="oe_subtotal_footer_separator" nolabel="1"/> </group> - <group> - <field name="pay_now" on_change="onchange_payment(pay_now, journal_id, partner_id)" required="1"/> - <field name="date_due" attrs="{'invisible':[('pay_now','=','pay_now')]}"/> - <field name="account_id" - attrs="{'invisible':[('pay_now','!=','pay_now')]}" - domain="[('type','=','liquidity')]"/> - <field name="reference" - attrs="{'invisible':[('pay_now','!=','pay_now')]}"/> - </group> </group> </page> <page string="Journal Items" attrs="{'invisible': [('state','!=','posted')]}"> @@ -224,11 +221,11 @@ <h1><label for="number" string="Purchase Receipt"/> <field name="number" class="oe_inline" readonly="1"/></h1> <field name="pay_now" invisible="1"/> - <field name="account_id" domain="[('type','=','other')]" invisible="True"/> <field name="type" invisible="True"/> <group> <group> <field name="partner_id" domain="[('supplier','=',True)]" string="Supplier" on_change="onchange_partner_id(partner_id, journal_id, amount, currency_id, type, date, context)" context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}" /> + <field name="account_id" domain="[('type', 'in', ['liquidity', 'payable'])]"/> <field name="name" colspan="2"/> <field name="reference"/> <field name="company_id" widget="selection" groups="base.group_multi_company"/> diff --git a/addons/account_voucher/wizard/account_statement_from_invoice.py b/addons/account_voucher/wizard/account_statement_from_invoice.py index de5121a819b..059989801d1 100644 --- a/addons/account_voucher/wizard/account_statement_from_invoice.py +++ b/addons/account_voucher/wizard/account_statement_from_invoice.py @@ -94,7 +94,7 @@ class account_statement_from_invoice_lines(osv.osv_memory): 'account_id': result['value'].get('account_id', statement.journal_id.default_credit_account_id.id), 'company_id': statement.company_id.id, 'currency_id': statement.currency.id, - 'date': line.date, + 'date': statement.date, 'amount': sign*amount, 'payment_rate': result['value']['payment_rate'], 'payment_rate_currency_id': result['value']['payment_rate_currency_id'], @@ -119,7 +119,7 @@ class account_statement_from_invoice_lines(osv.osv_memory): 'statement_id': statement_id, 'ref': line.ref, 'voucher_id': voucher_id, - 'date': time.strftime('%Y-%m-%d'), + 'date': statement.date, }, context=context) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/audittrail/audittrail.py b/addons/audittrail/audittrail.py index 159baf3a9e7..ca4a15bca6e 100644 --- a/addons/audittrail/audittrail.py +++ b/addons/audittrail/audittrail.py @@ -268,10 +268,14 @@ def log_fct(cr, uid_orig, model, method, fct_src, *args, **kw): new_values = get_data(cr, uid_orig, pool, res_ids, model, method) elif method == 'read': res = fct_src(cr, uid_orig, model.model, method, *args, **kw) + if isinstance(res, dict): + records = [res] + else: + records = res # build the res_ids and the old_values dict. Here we don't use get_data() to # avoid performing an additional read() res_ids = [] - for record in res: + for record in records: res_ids.append(record['id']) old_values[(model.id, record['id'])] = {'value': record, 'text': record} # log only the fields read @@ -279,7 +283,9 @@ def log_fct(cr, uid_orig, model, method, fct_src, *args, **kw): elif method == 'unlink': res_ids = args[0] old_values = get_data(cr, uid_orig, pool, res_ids, model, method) - res = fct_src(cr, uid_orig, model.model, method, *args, **kw) + # process_data first as fct_src will unlink the record + self.process_data(cr, uid_orig, pool, res_ids, model, method, old_values, new_values, field_list) + return fct_src(cr, uid_orig, model.model, method, *args, **kw) else: # method is write, action or workflow action res_ids = [] if args: @@ -322,7 +328,7 @@ def get_data(cr, uid, pool, res_ids, model, method): data = {} resource_pool = pool[model.model] # read all the fields of the given resources in super admin mode - for resource in resource_pool.read(cr, SUPERUSER_ID, res_ids): + for resource in resource_pool.read(cr, SUPERUSER_ID, res_ids, resource_pool._all_columns): values = {} values_text = {} resource_id = resource['id'] @@ -456,7 +462,9 @@ def process_data(cr, uid, pool, res_ids, model, method, old_values=None, new_val # if at least one modification has been found for model_id, resource_id in lines: - name = pool[model.model].name_get(cr, uid, [resource_id])[0][1] + line_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, model_id).model + name = pool.get(line_model).name_get(cr, uid, [resource_id])[0][1] + vals = { 'method': method, 'object_id': model_id, diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index e660fd8363b..53a6ffd5462 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -25,7 +25,7 @@ def fragment_to_query_string(func): return """<html><head><script> var l = window.location; var q = l.hash.substring(1); - var r = '/' + l.search; + var r = l.pathname + l.search; if(q.length !== 0) { var s = l.search ? (l.search === '?' ? '' : '&') : '?'; r = l.pathname + l.search + s + q; diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 3f53b28b075..08b67108c3f 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -36,7 +36,10 @@ class AuthSignup(openerp.addons.web.controllers.main.Home): def web_login(self, *args, **kw): mode = request.params.get('mode') qcontext = request.params.copy() - super_response = super(AuthSignup, self).web_login(*args, **kw) + super_response = None + if request.httprequest.method != 'POST' or mode not in ('reset', 'signup'): + # Default behavior is to try to login, which in reset or signup mode in a non-sense. + super_response = super(AuthSignup, self).web_login(*args, **kw) response = webmain.render_bootstrap_template(request.session.db, 'auth_signup.signup', qcontext, lazy=True) if isinstance(super_response, LazyResponse): response.params['values'].update(super_response.params['values']) @@ -62,7 +65,7 @@ class AuthSignup(openerp.addons.web.controllers.main.Home): } qcontext.update(config) - if 'error' in qcontext or mode not in ('reset', 'signup') or (not token and not config[mode]): + if 'error' in request.params or mode not in ('reset', 'signup') or (not token and not config[mode]): if isinstance(super_response, LazyResponse): super_response.params['values'].update(config) return super_response @@ -78,22 +81,28 @@ class AuthSignup(openerp.addons.web.controllers.main.Home): res_users.reset_password(request.cr, openerp.SUPERUSER_ID, login) qcontext['message'] = _("An email has been sent with credentials to reset your password") response.params['template'] = 'web.login' - except Exception: - qcontext['error'] = _("Could not reset your password") + except Exception, e: + qcontext['error'] = exception_to_unicode(e) or _("Could not reset your password") _logger.exception('error when resetting password') else: values = dict((key, qcontext.get(key)) for key in ('login', 'name', 'password')) try: self._signup_with_values(token, values) - request.cr.commit() + redirect = request.params.get('redirect') + if not redirect: + redirect = '/web?' + request.httprequest.query_string + return http.redirect_with_hash(redirect) except SignupError, e: qcontext['error'] = exception_to_unicode(e) - return super(AuthSignup, self).web_login(*args, **kw) return response def _signup_with_values(self, token, values): - request.registry['res.users'].signup(request.cr, openerp.SUPERUSER_ID, values, token) + db, login, password = request.registry['res.users'].signup(request.cr, openerp.SUPERUSER_ID, values, token) + request.cr.commit() # as authenticate will use its own cursor we need to commit the current transaction + uid = request.session.authenticate(db, login, password) + if not uid: + raise SignupError(_('Authentification Failed.')) # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py index 3af52017e05..e91af657cc2 100644 --- a/addons/auth_signup/res_users.py +++ b/addons/auth_signup/res_users.py @@ -25,7 +25,7 @@ from urlparse import urljoin from openerp.addons.base.ir.ir_mail_server import MailDeliveryException from openerp.osv import osv, fields -from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT, ustr from ast import literal_eval from openerp.tools.translate import _ @@ -236,7 +236,12 @@ class res_users(osv.Model): # create a copy of the template user (attached to a specific partner_id if given) values['active'] = True context = dict(context or {}, no_reset_password=True) - return self.copy(cr, uid, template_user_id, values, context=context) + try: + with cr.savepoint(): + return self.copy(cr, uid, template_user_id, values, context=context) + except Exception, e: + # copy may failed if asked login is not available. + raise SignupError(ustr(e)) def reset_password(self, cr, uid, login, context=None): """ retrieve the user corresponding to login (login or email), diff --git a/addons/auth_signup/views/auth_signup_login.xml b/addons/auth_signup/views/auth_signup_login.xml index a279528e9ca..a9e43c86f4f 100644 --- a/addons/auth_signup/views/auth_signup_login.xml +++ b/addons/auth_signup/views/auth_signup_login.xml @@ -40,7 +40,7 @@ <div class="form-group field-login"> <label for="login" class="control-label">Your Email</label> - <input type="text" name="login" t-att-value="login" id="login" class="form-control" + <input type="email" name="login" t-att-value="login" id="login" class="form-control" t-att-autofocus="'autofocus' if reset_without_token else None" required="required" t-att-disabled="'disabled' if mode == 'reset' and token else None"/> <input type="hidden" name="login" t-att-value="login" t-if="mode == 'reset' and token"/> diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py index 58728a77aef..e48533ebeb4 100644 --- a/addons/base_action_rule/base_action_rule.py +++ b/addons/base_action_rule/base_action_rule.py @@ -275,6 +275,13 @@ class base_action_rule(osv.osv): if action.filter_id: domain = eval(action.filter_id.domain) ctx.update(eval(action.filter_id.context)) + if 'lang' not in ctx: + # Filters might be language-sensitive, attempt to reuse creator lang + # as we are usually running this as super-user in background + [filter_meta] = action.filter_id.perm_read() + user_id = filter_meta['write_uid'] and filter_meta['write_uid'][0] or \ + filter_meta['create_uid'][0] + ctx['lang'] = self.pool['res.users'].browse(cr, uid, user_id).lang record_ids = model.search(cr, uid, domain, context=ctx) # determine when action should occur for the records diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py index 763d0784603..955591e8824 100644 --- a/addons/base_setup/res_config.py +++ b/addons/base_setup/res_config.py @@ -41,7 +41,9 @@ class base_config_settings(osv.osv_memory): help="""This installs the module google_docs."""), 'module_google_calendar': fields.boolean('Allow the users to synchronize their calendar with Google Calendar', help="""This installs the module google_calendar."""), - 'font': fields.many2one('res.font', string="Report Font", help="Set the font into the report header, it will be used as default font in the RML reports of the user company"), + 'font': fields.many2one('res.font', string="Report Font", domain=[('mode', 'in', ('Normal', 'Regular', 'all', 'Book'))], + help="Set the font into the report header, it will be used as default font in the RML reports of the user company"), + } _defaults= { diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml index 73fc9f83830..66fdcf91633 100644 --- a/addons/base_setup/res_config_view.xml +++ b/addons/base_setup/res_config_view.xml @@ -99,7 +99,7 @@ <label for="font" /> <div> <div> - <field name="font" class="oe_inline" domain="[('mode', 'in', ('normal', 'regular', 'all', 'book'))]" /> + <field name="font" class="oe_inline" /> <button string="(reload fonts)" name="act_discover_fonts" type="object" class="oe_link"/> </div> </div> diff --git a/addons/base_vat/base_vat.py b/addons/base_vat/base_vat.py index 52db18b7307..e9ecf70b39b 100644 --- a/addons/base_vat/base_vat.py +++ b/addons/base_vat/base_vat.py @@ -54,7 +54,7 @@ _ref_vat = { 'gr': 'GR12345670', 'hu': 'HU12345676', 'hr': 'HR01234567896', # Croatia, contributed by Milan Tribuson - 'ie': 'IE1234567T', + 'ie': 'IE1234567FA', 'it': 'IT12345670017', 'lt': 'LT123456715', 'lu': 'LU12345613', @@ -190,6 +190,34 @@ class res_partner(osv.osv): return check == int(num[8]) return False + def _ie_check_char(self, vat): + vat = vat.zfill(8) + extra = 0 + if vat[7] not in ' W': + if vat[7].isalpha(): + extra = 9 * (ord(vat[7]) - 64) + else: + # invalid + return -1 + checksum = extra + sum((8-i) * int(x) for i, x in enumerate(vat[:7])) + return 'WABCDEFGHIJKLMNOPQRSTUV'[checksum % 23] + + def check_vat_ie(self, vat): + """ Temporary Ireland VAT validation to support the new format + introduced in January 2013 in Ireland, until upstream is fixed. + TODO: remove when fixed upstream""" + if len(vat) not in (8, 9) or not vat[2:7].isdigit(): + return False + if len(vat) == 8: + # Normalize pre-2013 numbers: final space or 'W' not significant + vat += ' ' + if vat[:7].isdigit(): + return vat[7] == self._ie_check_char(vat[:7] + vat[8]) + elif vat[1] in (string.ascii_uppercase + '+*'): + # Deprecated format + # See http://www.revenue.ie/en/online/third-party-reporting/reporting-payment-details/faqs.html#section3 + return vat[7] == self._ie_check_char(vat[2:7] + vat[0] + vat[8]) + return False # Mexican VAT verification, contributed by <moylop260@hotmail.com> # and Panos Christeas <p_christ@hol.gr> diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index a8efc343420..ca4d00eb85e 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -19,13 +19,13 @@ # ############################################################################## -import hashlib import pytz import re import time import openerp import openerp.service.report import uuid +from werkzeug.exceptions import BadRequest from datetime import datetime, timedelta from dateutil import parser from dateutil import rrule @@ -34,10 +34,11 @@ from openerp import tools, SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.translate import _ -from openerp import http from openerp.http import request from operator import itemgetter +from werkzeug.exceptions import BadRequest + import logging _logger = logging.getLogger(__name__) @@ -776,7 +777,7 @@ class calendar_event(osv.Model): else: result[event] = "" return result - + def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None): if not isinstance(ids, list): ids = [ids] @@ -789,7 +790,7 @@ class calendar_event(osv.Model): data.update(update_data) self.write(cr, uid, ids, data, context=context) return True - + def _tz_get(self, cr, uid, context=None): return [(x.lower(), x) for x in pytz.all_timezones] @@ -802,7 +803,7 @@ class calendar_event(osv.Model): }, } _columns = { - 'id': fields.integer('ID', readonly=True), + 'id': fields.integer('ID', readonly=True), 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'), 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}), 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'), @@ -844,6 +845,7 @@ class calendar_event(osv.Model): 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'), 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}), 'alarm_ids': fields.many2many('calendar.alarm', string='Reminders', ondelete="restrict"), + } _defaults = { 'end_type': 'count', @@ -870,11 +872,9 @@ class calendar_event(osv.Model): ] def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None): + """Returns duration and/or end date based on values passed @param ids: List of calendar event's IDs. - @param start_date: Starting date - @param duration: Duration between start date and end date - @param end_date: Ending Datee """ if context is None: context = {} @@ -888,14 +888,14 @@ class calendar_event(osv.Model): value['duration'] = duration if allday: # For all day event - start = datetime.strptime(start_date.split(' ')[0].split('T')[0], "%Y-%m-%d") - duration = 24.0 - value['duration'] = duration + start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") user = self.pool['res.users'].browse(cr, uid, uid) tz = pytz.timezone(user.tz) if user.tz else pytz.utc start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone start = start.astimezone(pytz.utc) # convert start back to utc - value['date'] = start.strftime("%Y-%m-%d") + ' 00:00:00' + + value['duration'] = 24.0 + value['date'] = datetime.strftime(start, "%Y-%m-%d %H:%M:%S") else: start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") @@ -1353,15 +1353,55 @@ class calendar_event(osv.Model): res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context) return res + def _detach_one_event(self, cr, uid, id, values=dict(), context=None): + real_event_id = calendar_id2real_id(id) + data = self.read(cr, uid, id, ['date', 'date_deadline', 'rrule', 'duration']) + + if data.get('rrule'): + data.update( + values, + recurrent_id=real_event_id, + recurrent_id_date=data.get('date'), + rrule_type=False, + rrule='', + recurrency=False, + end_date = datetime.strptime(values.get('date', False) or data.get('date'),"%Y-%m-%d %H:%M:%S") + + timedelta(hours=values.get('duration', False) or data.get('duration')) + ) + + #do not copy the id + if data.get('id'): + del(data['id']) + new_id = self.copy(cr, uid, real_event_id, default=data, context=context) + return new_id + + def open_after_detach_event(self, cr, uid, ids, context=None): + if context is None: + context = {} + + new_id = self._detach_one_event(cr, uid, ids[0], context=context) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'calendar.event', + 'view_mode': 'form', + 'res_id': new_id, + 'target': 'current', + 'flags': {'form': {'action_buttons': True, 'options' : { 'mode' : 'edit' } } } + } + + + + def write(self, cr, uid, ids, values, context=None): def _only_changes_to_apply_on_real_ids(field_names): - ''' return True if changes are only to be made on the real ids''' + ''' return True if changes are only to be made on the real ids''' for field in field_names: - if field not in ['name', 'message_follower_ids','oe_update_date']: - return False - return True + if field in ['date','active']: + return True + return False context = context or {} + if isinstance(ids, (str,int, long)): if len(str(ids).split('-')) == 1: @@ -1383,31 +1423,14 @@ class calendar_event(osv.Model): # if we are setting the recurrency flag to False or if we are only changing fields that # should be only updated on the real ID and not on the virtual (like message_follower_ids): # then set real ids to be updated. - if not values.get('recurrency', True) or _only_changes_to_apply_on_real_ids(values.keys()): + if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()): ids.append(real_event_id) continue - - #if edit one instance of a reccurrent id - data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration']) - if data.get('rrule'): - data.update( - values, - recurrent_id=real_event_id, - recurrent_id_date=data.get('date'), - rrule_type=False, - rrule='', - recurrency=False, - end_date = datetime.strptime(values.get('date', False) or data.get('date'),"%Y-%m-%d %H:%M:%S") - + timedelta(hours=values.get('duration', False) or data.get('duration')) - ) - - #do not copy the id - if data.get('id'): - del(data['id']) - new_id = self.copy(cr, uid, real_event_id, default=data, context=context) - context.update({'active_id': new_id, 'active_ids': [new_id]}) - continue - + else: + data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration']) + if data.get('rrule'): + new_id = self._detach_one_event(cr, uid, event_id, values, context=None) + res = super(calendar_event, self).write(cr, uid, ids, values, context=context) # set end_date for calendar searching @@ -1473,7 +1496,6 @@ class calendar_event(osv.Model): if context is None: context = {} fields2 = fields and fields[:] or None - EXTRAFIELDS = ('class', 'user_id', 'duration', 'date', 'rrule', 'vtimezone') for f in EXTRAFIELDS: if fields and (f not in fields): @@ -1518,6 +1540,7 @@ class calendar_event(osv.Model): for k in EXTRAFIELDS: if (k in r) and (fields and (k not in fields)): del r[k] + if isinstance(ids, (str, int, long)): return result and result[0] or False return result @@ -1531,7 +1554,7 @@ class calendar_event(osv.Model): ids_to_unlink = [] # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it - for event_id in ids: + for event_id in ids: if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL if self.browse(cr, uid, event_id).recurrent_id: ids_to_exclure.append(event_id) diff --git a/addons/calendar/calendar_demo.xml b/addons/calendar/calendar_demo.xml index 6880754cc56..9e0b44c89cb 100644 --- a/addons/calendar/calendar_demo.xml +++ b/addons/calendar/calendar_demo.xml @@ -6,20 +6,7 @@ --> <!--For Meetings --> - <record id="res_partner_another" model="res.partner"> - <field name="name">Arshaw</field> - <field name="company_id" ref="base.main_company"/> - <field name="customer" eval="False"/> - <field name="email">fullcalendar@example.com</field> - </record> - <record id="res_user_another" model="res.users"> - <field name="name" >Second Demo User</field> - <field name="login" >Second Demo User</field> - <field name="partner_id" ref="res_partner_another"/> - <field name="company_id" ref="base.main_company" /> - </record> - - <record id="cal_contact_1" model="calendar.contacts"> + <record id="cal_contact_1" model="calendar.contacts"> <field eval="1" name="active"/> <field name="user_id" ref="base.user_root"/> <field name="partner_id" ref="base.res_partner_1"/> @@ -105,8 +92,8 @@ <record id="calendar_event_7" model="calendar.event"> <field eval="1" name="active"/> - <field name="user_id" ref="res_user_another"/> - <field name="partner_ids" eval="[(6,0,[ref('res_partner_another'),ref('base.res_partner_8')])]"/> + <field name="user_id" ref="base.user_demo"/> + <field name="partner_ids" eval="[(6,0,[ref('base.res_partner_7')])]"/> <field name="name">Presentation of the new Calendar</field> <field name="categ_ids" eval="[(6,0,[ref('categ_meet1'), ref('categ_meet2')])]"/> <field eval="time.strftime('%Y-%m-16 6:00:00')" name="date"/> @@ -114,18 +101,7 @@ <field eval="8.5" name="duration"/> <field name="state">draft</field> </record> - - <record id="calendar_event_8" model="calendar.event"> - <field eval="1" name="active"/> - <field name="user_id" ref="res_user_another"/> - <field name="partner_ids" eval="[(6,0,[ref('res_partner_another'),ref('base.partner_root')])]"/> - <field name="name">Discuss about the module : Calendar </field> - <field name="categ_ids" eval="[(6,0,[ref('categ_meet1'), ref('categ_meet2')])]"/> - <field eval="time.strftime('%Y-%m-16 6:00:00')" name="date"/> - <field eval="time.strftime('%Y-%m-16 18:30:00')" name="date_deadline"/> - <field eval="8.5" name="duration"/> - <field name="state">draft</field> - </record> + </data> </openerp> diff --git a/addons/calendar/calendar_view.xml b/addons/calendar/calendar_view.xml index ddcf41af352..7e1b6a55039 100644 --- a/addons/calendar/calendar_view.xml +++ b/addons/calendar/calendar_view.xml @@ -51,21 +51,26 @@ </div> <notebook> <page string="Meeting Details"> + <group attrs="{'invisible': [('recurrency','==',False)]}" class="oe_edit_only "> + <p class='alert alert-warning'> This event is linked to a recurrence...<br/> + <button type="object" name="open_after_detach_event" string="Update only this instance" help="Click here to update only this instance and not all recurrences. " class="oe_link"/> + </p> + </group> <group> <group> <field name="date" string="Starting at" on_change="onchange_dates(date, duration, False, allday)"/> <label for="duration"/> <div> <field name="duration" widget="float_time" - on_change="onchange_dates(date,duration,False,allday)" + on_change="onchange_dates(date, duration, False, allday)" class="oe_inline" attrs="{'invisible': [('allday','=',True)]}"/> <label string="hours" attrs="{'invisible': [('allday','=',True)]}"/> - (<field name="allday" on_change="onchange_dates(date,False,False,allday)" class="oe_inline"/> + (<field name="allday" class="oe_inline"/> <label for="allday" string="All Day?"/>) </div> <field name="date_deadline" groups="base.group_no_one" attrs="{'invisible': ['|', ('allday','=',True), ('duration','<', 24)]}" - on_change="onchange_dates(date,False,date_deadline)"/> + /> </group> <group> <field name="categ_ids" widget="many2many_tags"/> @@ -121,10 +126,10 @@ </div> </group> </group> - <group> + <group> <field name="class"/> <field name="show_as"/> - <field name="rrule" invisible="1" readonly="0" /> + <field name="rrule" invisible="1" readonly="0" /> <field name="recurrent_id" invisible="1" /> </group> </group> @@ -136,7 +141,7 @@ <tree string="Invitation details" editable="top" create="false" delete="false"> <field name="partner_id" /> <field name="state" /> - <field name="email" widget="email"/> + <field name="email" widget="email"/> <button name="do_tentative" states="needsAction,declined,accepted" string="Uncertain" type="object" icon="terp-crm" /> <button name="do_accept" string="Accept" states="needsAction,tentative,declined" type="object" icon="gtk-apply"/> @@ -176,7 +181,7 @@ <field name="date" string="Start" /> <field name="duration" string="Duration" widget="float_time" attrs="{'invisible': [('allday','=',True)]}"/> <field name="allday" class="oe_inline" attrs="{'invisible': [('allday','=',False)]}"/> - <field name="partner_ids" widget="many2manyattendee" string="Attendees"/> + <field name="partner_ids" widget="many2manyattendee" string="Attendees"/> </group> <group> <field name="categ_ids" widget="many2many_tags"/> @@ -213,7 +218,7 @@ <field name="name">Meetings Calendar</field> <field name="model">calendar.event</field> <field name="priority" eval="2"/> - <field name="arch" type="xml"> + <field name="arch" type="xml"> <calendar string="Meetings" date_start="date" date_stop="date_deadline" date_delay="duration" all_day="allday" display="[name]" color="color_partner_id" attendee="partner_ids" avatar_model="res.partner" use_contacts="True" event_open_popup="%(calendar.view_calendar_event_form_popup)s"> @@ -248,14 +253,14 @@ <field name="user_id"/> <field name="show_as"/> <field name="class" string="Privacy"/> - <filter icon="terp-go-today" string="My Events" domain="[('user_id','=',uid)]" help="My Events"/> + <filter icon="terp-go-today" string="My Events" domain="[('user_id','=',uid)]" help="My Events"/> <filter string="My Meetings" help="My Meetings" name="mymeetings" context='{"mymeetings": 1}'/> <filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/> - <separator/> + <separator/> <group expand="0" string="Group By..."> <filter string="Responsible" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/> <filter string="Availability" icon="terp-camera_test" domain="[]" context="{'group_by':'show_as'}"/> - <filter string="Privacy" icon="terp-locked" domain="[]" context="{'group_by':'class'}"/> + <filter string="Privacy" icon="terp-locked" domain="[]" context="{'group_by':'class'}"/> <filter string="Event Month" icon="terp-go-month" domain="[]" context="{'group_by':'date'}" help="Start Date of Event by Month"/> </group> </search> @@ -267,8 +272,7 @@ <field name="res_model">calendar.event</field> <field name="view_mode">calendar,tree,form,gantt</field> <field name="view_id" ref="view_calendar_event_calendar"/> - <field name="search_view_id" ref="view_calendar_event_search"/> - <field name="context">{"search_default_mymeetings": 1}</field> + <field name="search_view_id" ref="view_calendar_event_search"/> <field name="help" type="html"> <p class="oe_view_nocontent_create"> Click to schedule a new meeting. @@ -313,7 +317,7 @@ <field name="name">Meetings</field> <field name="res_model">calendar.event</field> <field name="view_mode">form,calendar,tree,gantt</field> - <field name="view_id" ref="view_calendar_event_form"/> + <field name="view_id" ref="view_calendar_event_form"/> </record> diff --git a/addons/calendar/contacts.py b/addons/calendar/contacts.py index 2cd42b55d37..dc964554629 100644 --- a/addons/calendar/contacts.py +++ b/addons/calendar/contacts.py @@ -26,7 +26,7 @@ class calendar_contacts(osv.osv): _columns = { 'user_id': fields.many2one('res.users','Me'), - 'partner_id': fields.many2one('res.partner','Employee',required=True, domain=[('customer','=',True)]), + 'partner_id': fields.many2one('res.partner','Employee',required=True, domain=[]), 'active':fields.boolean('active'), } _defaults = { diff --git a/addons/calendar/contacts_view.xml b/addons/calendar/contacts_view.xml index cf685f88d00..1d014fea1f6 100644 --- a/addons/calendar/contacts_view.xml +++ b/addons/calendar/contacts_view.xml @@ -16,6 +16,7 @@ <field name="res_model">calendar.contacts</field> <field name="view_type">form</field> <field name="view_mode">tree</field> + <field name="domain">[('user_id','=',uid)]</field> <field name="view_id" ref="view_calendar_contacts" /> <field name="help" type="html"> <p class="oe_view_nocontent_create"> diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 0f5693eacbc..18fd4dfa20e 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -1,7 +1,7 @@ openerp.calendar = function(instance) { var _t = instance.web._t; var QWeb = instance.web.qweb; - instance.calendar = {} + instance.calendar = {}; instance.web.WebClient = instance.web.WebClient.extend({ @@ -11,8 +11,8 @@ openerp.calendar = function(instance) { get_next_notif: function() { var self= this; this.rpc("/calendar/notify") - .then( - function(result) { + .then( + function(result) { _.each(result, function(res) { setTimeout(function() { //If notification not already displayed, we add button and action on it @@ -21,25 +21,25 @@ openerp.calendar = function(instance) { res.message += QWeb.render("notify_footer"); a = self.do_notify(res.title,res.message,true); - $(".link2event").on('click', function() { + $(".link2event").on('click', function() { self.rpc("/web/action/load", { action_id: "calendar.action_calendar_event_notify", - }).then( function(r) { + }).then( function(r) { r.res_id = res.event_id; - return self.action_manager.do_action(r); - }); + return self.action_manager.do_action(r); + }); }); - a.element.find(".link2recall").on('click',function() { + a.element.find(".link2recall").on('click',function() { self.get_notif_box(this).find('.ui-notify-close').trigger("click"); - }); - a.element.find(".link2showed").on('click',function() { + }); + a.element.find(".link2showed").on('click',function() { self.get_notif_box(this).find('.ui-notify-close').trigger("click"); self.rpc("/calendar/notify_ack"); }); } //If notification already displayed in the past, we remove the css attribute which hide this notification else if (self.get_notif_box($.find(".eid_"+res.event_id)).attr("style") !== ""){ - self.get_notif_box($.find(".eid_"+res.event_id)).attr("style",""); + self.get_notif_box($.find(".eid_"+res.event_id)).attr("style",""); } },res.timer * 1000); }); @@ -48,7 +48,7 @@ openerp.calendar = function(instance) { }, check_notifications: function() { var self= this; - self.get_next_notif(); + self.get_next_notif(); setInterval(function(){ self.get_next_notif(); }, 5 * 60 * 1000 ); @@ -58,7 +58,7 @@ openerp.calendar = function(instance) { show_application: function() { this._super(); this.check_notifications(); - } + }, }); @@ -77,7 +77,6 @@ openerp.calendar = function(instance) { if(instance.session.session_is_valid(self.db) && instance.session.username != "anonymous") { self.redirect_meeting_view(self.db,self.action,self.id,self.view); } else { - alert('in anonymous or null '); self.open_invitation_form(self.attendee_data); } }, @@ -92,9 +91,8 @@ openerp.calendar = function(instance) { var reload_page = function(){ return location.replace(action_url); - } + }; reload_page(); - }, }); @@ -108,7 +106,7 @@ openerp.calendar = function(instance) { }); }, map_tag: function(value){ - return _.map(value, function(el) {return {name: el[1], id:el[0], state: el[2]};}) + return _.map(value, function(el) {return {name: el[1], id:el[0], state: el[2]};}); }, get_render_data: function(ids){ var self = this; @@ -121,7 +119,7 @@ openerp.calendar = function(instance) { if (! self.get("effective_readonly")) { var tag_element = self.tags.tagElements(); _.each(data,function(value, key){ - $(tag_element[key]).find(".custom-edit").addClass(data[key][2]) + $(tag_element[key]).find(".custom-edit").addClass(data[key][2]); }); } } @@ -134,7 +132,7 @@ openerp.calendar = function(instance) { instance.session.session_bind(instance.session.origin).done(function () { new instance.calendar.invitation(null,db,action,id,view,attendee_data).appendTo($("body").addClass('openerp')); }); - } + }; diff --git a/addons/calendar/test/calendar_test.yml b/addons/calendar/test/calendar_test.yml index 4888c94a0c5..1454a635a4f 100644 --- a/addons/calendar/test/calendar_test.yml +++ b/addons/calendar/test/calendar_test.yml @@ -33,12 +33,14 @@ !python {model: calendar.event}: | ids = self.search(cr, uid, [('date', '>=', '2011-04-30 16:00:00'), ('date', '<=', '2011-05-31 00:00:00')], context={'virtual_id': True} ) before = self.search(cr, uid, [('date', '>=', '2011-04-30 16:00:00'), ('date', '<=', '2011-05-31 00:00:00')], context={'virtual_id': False}) - self.write(cr, uid,[ids[1]], {'name':'New Name','recurrency' : True}, context={'virtual_id': True}) + # We start by detach the event + newid = self._detach_one_event(cr, uid,ids[1]) + self.write(cr, uid,[newid], {'name':'New Name','recurrency' : True}, context={'virtual_id': True}) after = self.search(cr, uid, [('date', '>=', '2011-04-30 16:00:00'), ('date', '<=', '2011-05-31 00:00:00')], context={'virtual_id': False}) assert len(after) == len(before)+1, 'Wrong number of events found, after to have moved a virtual event' - new_id = list(set(after)-set(before))[0] + new_id = list(set(after)-set(before))[0] new_event = self.browse(cr,uid,new_id,context=context) - assert new_event.recurrent_id == before[0], 'Recurrent_id not correctly passed to the new event' + assert new_event.recurrent_id == before[0], 'Recurrent_id not correctly passed to the new event' - Now I will make All day event and test it - @@ -58,7 +60,6 @@ duration: 1 interval: days type: notification - - Now I will assign this reminder to all day event - diff --git a/addons/calendar/test/test_calendar_recurrent_event_case2.yml b/addons/calendar/test/test_calendar_recurrent_event_case2.yml index 6aedd3d966d..0d300cf7a2a 100644 --- a/addons/calendar/test/test_calendar_recurrent_event_case2.yml +++ b/addons/calendar/test/test_calendar_recurrent_event_case2.yml @@ -84,9 +84,12 @@ idval = '%d-%s' % (ref('calendar_event_sprintreview0'), '20110425124700') self.write(cr, uid, [idval], {'description': 'Review code of the module: sync_google_calendar.'}) - - I check whether the record is edited perfectly or not. + I check whether that all the records of this recurrence has been edited. - !python {model: calendar.event}: | - meeting_ids = self.search(cr, uid, [('recurrent_id', '=', ref('calendar_event_sprintreview0')), ('recurrent_id_date','=','2011-04-25 12:47:00')], context) - assert meeting_ids, 'Meeting is not edited !' + meeting_ids = self.search(cr, uid, [('recurrent_id', '=', ref('calendar_event_sprintreview0'))], context) + meetings = self.browse(cr, uid, meeting_ids, context) + for meeting in meetings: + assert meeting.description == 'Review code of the module: sync_google_calendar.', 'Description not changed for id: %s' %meeting.id + diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 28b8469580e..20e2a6e8aff 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -120,14 +120,14 @@ class crm_case_section(osv.osv): month_begin = date.today().replace(day=1) section_result = [{ 'value': 0, - 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'), + '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_begin_date.strftime('%B')} + 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): @@ -140,13 +140,18 @@ class crm_case_section(osv.osv): 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]) - date_domain = [('create_date', '>=', date_begin.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)), ('create_date', '<=', date_end.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))] + 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 = date_domain + [('type', '=', 'lead'), ('section_id', '=', id)] + 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) - opp_domain = date_domain + [('type', '=', 'opportunity'), ('section_id', '=', id)] - res[id]['monthly_planned_revenue'] = self.__get_bar_values(cr, uid, obj, opp_domain, ['planned_revenue', 'create_date'], 'planned_revenue', '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 = { diff --git a/addons/crm/crm_case_section_view.xml b/addons/crm/crm_case_section_view.xml index 4bc74ce880a..d3ad226dbea 100644 --- a/addons/crm/crm_case_section_view.xml +++ b/addons/crm/crm_case_section_view.xml @@ -63,9 +63,9 @@ <record id="action_report_crm_lead_salesteam" model="ir.actions.act_window"> <field name="name">Leads Analysis</field> <field name="res_model">crm.lead.report</field> - <field name="view_type">form</field> <field name="context">{"search_default_month":1}</field> - <field name="view_mode">tree,graph</field> + <field name="view_mode">graph</field> + <field name="view_id" ref="crm.view_report_crm_lead_graph_two"/> <field name="domain">[('type','=', 'lead'),('section_id', '=', active_id)]</field> <field name="help">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.</field> </record> @@ -73,9 +73,8 @@ <record id="action_report_crm_opportunity_salesteam" model="ir.actions.act_window"> <field name="name">Opportunities Analysis</field> <field name="res_model">crm.lead.report</field> - <field name="view_type">form</field> - <field name="context">{"search_default_month":1}</field> - <field name="view_mode">tree,graph</field> + <field name="view_mode">graph</field> + <field name="view_id" ref="crm.view_report_crm_opportunity_graph"/> <field name="domain">[('type','=', 'opportunity'), ('section_id', '=', active_id)]</field> <field name="help">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.</field> </record> @@ -117,14 +116,14 @@ <a name="%(crm_case_form_view_salesteams_lead)d" type="action">Leads</a> <a name="%(action_report_crm_lead_salesteam)d" type="action" class="oe_sparkline_bar_link"> <field name="monthly_open_leads" widget="sparkline_bar" - options="{'height': '20px', 'barWidth': 4, 'barSpacing': 1, 'delayIn': '3000', 'tooltip_suffix': 'Leads'}">Open Leads per Month<br/>Click to see a detailed analysis of leads.</field> + options="{'height': '20px', 'barWidth': 4, 'barSpacing': 1, 'delayIn': '3000', 'tooltip_suffix': ' Leads'}">Open Leads per Month<br/>Click to see a detailed analysis of leads.</field> </a> </div> <div class="oe_salesteams_opportunities"> <a name="%(crm_case_form_view_salesteams_opportunity)d" type="action">Opportunities</a> <a name="%(action_report_crm_opportunity_salesteam)d" type="action"> <field name="monthly_planned_revenue" widget="sparkline_bar" - options="{'height': '20px', 'barWidth': '4', 'barSpacing': '1', 'delayIn': '3000', 'tooltip_suffix': 'Opportunities'}">Planned Revenue per Month<br/>Click to see a detailed analysis of opportunities.</field> + options="{'height': '20px', 'barWidth': '4', 'barSpacing': '1', 'delayIn': '3000', 'tooltip_suffix': ' (Planned Revenue)'}">Planned Revenue per Month<br/>Click to see a detailed analysis of opportunities.</field> </a> </div> </div> diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index b89c04a7d36..6f522f0118f 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -330,6 +330,7 @@ class crm_lead(format_address, osv.osv): 'phone': partner.phone, 'mobile': partner.mobile, 'fax': partner.fax, + 'zip': partner.zip, } return {'value': values} diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index c728404d9ce..e76a7a9124d 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -343,6 +343,7 @@ help="Leads that are assigned to any sales teams I am member of"/> <filter string="Dead" name="dead" domain="[('probability', '=', '0'), ('stage_id.fold', '=', True)]"/> + <filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/> <separator /> <filter string="Available for mass mailing" name='not_opt_out' domain="[('opt_out', '=', False)]" @@ -562,6 +563,7 @@ <filter string="My Team(s)" domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}" help="Opportunities that are assigned to any sales teams I am member of"/> + <filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/> <separator/> <group expand="0" string="Group By..." colspan="16"> <filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/> diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml index a027a910520..9675913a0b5 100644 --- a/addons/crm/crm_phonecall_view.xml +++ b/addons/crm/crm_phonecall_view.xml @@ -171,6 +171,7 @@ <filter icon="terp-gtk-go-back-rtl" string="To Do" name="current" domain="[('state','=','open')]"/> <separator/> <filter string="Unassigned Phonecalls" icon="terp-personal-" domain="[('user_id','=',False)]" help="Unassigned Phonecalls"/> + <filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/> <separator/> <filter string="Phone Calls Assigned to Me or My Team(s)" icon="terp-personal+" domain="['|', ('section_id.user_id','=',uid), ('user_id', '=', uid)]" help="Phone Calls Assigned to the current user or with a team having the current user as team leader"/> diff --git a/addons/crm/report/crm_lead_report.py b/addons/crm/report/crm_lead_report.py index 871bef2099d..3a0786da5c7 100644 --- a/addons/crm/report/crm_lead_report.py +++ b/addons/crm/report/crm_lead_report.py @@ -89,10 +89,6 @@ class crm_lead_report(osv.osv): id, c.date_deadline, - to_char(c.create_date, 'YYYY') as creation_year, - to_char(c.create_date, 'MM') as creation_month, - to_char(c.create_date, 'YYYY-MM-DD') as creation_day, - to_char(c.date_open, 'YYYY-MM-DD') as opening_date, to_char(c.date_closed, 'YYYY-mm-dd') as date_closed, diff --git a/addons/crm/report/crm_lead_report_view.xml b/addons/crm/report/crm_lead_report_view.xml index f25cfa1f4b1..c4cffdc012a 100644 --- a/addons/crm/report/crm_lead_report_view.xml +++ b/addons/crm/report/crm_lead_report_view.xml @@ -15,6 +15,17 @@ </field> </record> + <record id="view_report_crm_lead_graph_two" model="ir.ui.view"> + <field name="name">crm.lead.report.graph.two</field> + <field name="model">crm.lead.report</field> + <field name="arch" type="xml"> + <graph string="Leads Analysis" type="pivot" stacked="True"> + <field name="create_date" type="row"/> + <field name="user_id" type="col"/> + </graph> + </field> + </record> + <record id="view_report_crm_opportunity_graph" model="ir.ui.view"> <field name="name">crm.opportunity.report.graph</field> <field name="model">crm.lead.report</field> @@ -23,7 +34,7 @@ <field name="date_deadline" type="row"/> <field name="user_id" type="col"/> <field name="stage_id" type="col"/> - <field name="probable_revenue" type="measure"/> + <field name="planned_revenue" type="measure"/> </graph> </field> </record> @@ -84,7 +95,7 @@ <filter string="Creation date (week)" icon="terp-go-year" domain="[]" context="{'group_by':'create_date:week'}"/> <filter string="Creation date (month)" icon="terp-go-year" - domain="[]" context="{'group_by':'create_date:month'}"/> + domain="[]" context="{'group_by':'create_date:month'}" name="month"/> <filter string="Creation date (year)" icon="terp-go-year" domain="[]" context="{'group_by':'create_date:year'}"/> <separator orientation="vertical" /> diff --git a/addons/crm_partner_assign/crm_portal_view.xml b/addons/crm_partner_assign/crm_portal_view.xml index 2b2b61c8425..dab831dcb4b 100644 --- a/addons/crm_partner_assign/crm_portal_view.xml +++ b/addons/crm_partner_assign/crm_portal_view.xml @@ -140,8 +140,8 @@ </group> <group col="2"> <separator string="Categorization" colspan="2"/> - <field name="type_id" widget="selection" readonly="1"/> - <field name="channel_id" widget="selection" readonly="1"/> + <field name="type_id" readonly="1"/> + <field name="channel_id" readonly="1"/> </group> </group> <separator string="Details" /> diff --git a/addons/email_template/email_template_view.xml b/addons/email_template/email_template_view.xml index 5429341e154..4f7828cb531 100644 --- a/addons/email_template/email_template_view.xml +++ b/addons/email_template/email_template_view.xml @@ -60,7 +60,7 @@ <h3>Body</h3> <field name="body_html" width="250" height="450" placeholder="Rich-text/HTML content of the message (placeholders may be used here)"/> - <field name="attachment_ids" nolabel="1" widget="many2many_binary"/> + <field name="attachment_ids" widget="many2many_binary"/> </page> <page string="Advanced Settings"> <group> diff --git a/addons/event/event.py b/addons/event/event.py index 0b10b1e51ec..d13d455bc73 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -219,6 +219,7 @@ class event_event(osv.osv): ] def onchange_event_type(self, cr, uid, ids, type_event, context=None): + values = {} if type_event: type_info = self.pool.get('event.type').browse(cr,uid,type_event,context) dic ={ @@ -228,7 +229,8 @@ class event_event(osv.osv): 'seats_min': type_info.default_registration_min, 'seats_max': type_info.default_registration_max, } - return {'value': dic} + values.update(dic) + return values def onchange_start_date(self, cr, uid, ids, date_begin=False, date_end=False, context=None): res = {'value':{}} diff --git a/addons/fetchmail/fetchmail.py b/addons/fetchmail/fetchmail.py index cce1f77856e..5f6379025ae 100644 --- a/addons/fetchmail/fetchmail.py +++ b/addons/fetchmail/fetchmail.py @@ -187,7 +187,7 @@ openerp_mailgate: "|/path/to/openerp-mailgate.py --host=localhost -u %(uid)d -p for server in self.browse(cr, uid, ids, context=context): _logger.info('start checking for new emails on %s server %s', server.type, server.name) context.update({'fetchmail_server_id': server.id, 'server_type': server.type}) - count = 0 + count, failed = 0, 0 imap_server = False pop_server = False if server.type == 'imap': @@ -196,20 +196,26 @@ openerp_mailgate: "|/path/to/openerp-mailgate.py --host=localhost -u %(uid)d -p imap_server.select() result, data = imap_server.search(None, '(UNSEEN)') for num in data[0].split(): + res_id = None result, data = imap_server.fetch(num, '(RFC822)') - res_id = mail_thread.message_process(cr, uid, server.object_id.model, - data[0][1], - save_original=server.original, - strip_attachments=(not server.attach), - context=context) + imap_server.store(num, '-FLAGS', '\\Seen') + try: + res_id = mail_thread.message_process(cr, uid, server.object_id.model, + data[0][1], + save_original=server.original, + strip_attachments=(not server.attach), + context=context) + except Exception: + _logger.exception('Failed to process mail from %s server %s.', server.type, server.name) + failed += 1 if res_id and server.action_id: - action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id], 'active_model': context.get("thread_model", server.object_id.model)}) - imap_server.store(num, '+FLAGS', '\\Seen') - cr.commit() + action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids': [res_id], 'active_model': context.get("thread_model", server.object_id.model)}) + imap_server.store(num, '+FLAGS', '\\Seen') + cr.commit() count += 1 - _logger.info("fetched/processed %s email(s) on %s server %s", count, server.type, server.name) + _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed) except Exception: - _logger.exception("Failed to fetch mail from %s server %s.", server.type, server.name) + _logger.exception("General failure when trying to fetch mail from %s server %s.", server.type, server.name) finally: if imap_server: imap_server.close() @@ -222,18 +228,23 @@ openerp_mailgate: "|/path/to/openerp-mailgate.py --host=localhost -u %(uid)d -p for num in range(1, numMsgs + 1): (header, msges, octets) = pop_server.retr(num) msg = '\n'.join(msges) - res_id = mail_thread.message_process(cr, uid, server.object_id.model, - msg, - save_original=server.original, - strip_attachments=(not server.attach), - context=context) + res_id = None + try: + res_id = mail_thread.message_process(cr, uid, server.object_id.model, + msg, + save_original=server.original, + strip_attachments=(not server.attach), + context=context) + except Exception: + _logger.exception('Failed to process mail from %s server %s.', server.type, server.name) + failed += 1 if res_id and server.action_id: - action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id], 'active_model': context.get("thread_model", server.object_id.model)}) + action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids': [res_id], 'active_model': context.get("thread_model", server.object_id.model)}) pop_server.dele(num) cr.commit() - _logger.info("fetched/processed %s email(s) on %s server %s", numMsgs, server.type, server.name) + _logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", numMsgs, server.type, server.name, (numMsgs - failed), failed) except Exception: - _logger.exception("Failed to fetch mail from %s server %s.", server.type, server.name) + _logger.exception("General failure when trying to fetch mail from %s server %s.", server.type, server.name) finally: if pop_server: pop_server.quit() diff --git a/addons/gamification/html/index.html b/addons/gamification/html/index.html deleted file mode 100644 index 890efa0d1ec..00000000000 --- a/addons/gamification/html/index.html +++ /dev/null @@ -1,86 +0,0 @@ -<section class="oe_container"> - <div class="oe_row oe_spaced"> - <div class="oe_span12"> - <h2 class="oe_slogan">Drive Engagement with Gamification</h2> - <h3 class="oe_slogan">Leverage natural desire for competition</h3> - <p class="oe_mt32"> - Reinforce good habits and improve win rates with real-time recognition and rewards inspired by <a href="http://en.wikipedia.org/wiki/Gamification">game mechanics</a>. Align teams around clear business objectives with challenges, personal objectives and team leader boards. - </p> - <div class="oe_span4 oe_centered"> - <h3>Leaderboards</h3> - <div class="oe_row_img oe_centered"> - <img class="oe_picture" src="crm_game_01.png"> - </div> - <p> - Promote leaders and competition amongst sales team with performance ratios. - </p> - </div> - <div class="oe_span4 oe_centered"> - <h3>Personnal Objectives</h3> - <div class="oe_row_img"> - <img class="oe_picture" src="crm_game_02.png"> - </div> - <p> - Assign clear goals to users to align them with the company objectives. - </p> - </div> - <div class="oe_span4 oe_centered"> - <h3>Visual Information</h3> - <div class="oe_row_img oe_centered"> - <img class="oe_picture" src="crm_game_03.png"> - </div> - <p> - See in an glance the progress of each user. - </p> - </div> - </div> -</section> - - -<section class="oe_container oe_dark"> - <div class="oe_row oe_spaced"> - <h2 class="oe_slogan">Create custom Challenges</h2> - <div class="oe_span6"> - <p class="oe_mt32"> -Use predefined goals to generate easily your own challenges. Assign it to a team or individual users. Receive feedback as often as needed: daily, weekly... Repeat it automatically to compare progresses through time. - </p> - </div> - <div class="oe_span6"> - <div class="oe_row_img oe_centered"> - <img class="oe_picture oe_screenshot" src="crm_sc_05.png"> - </div> - </div> - </div> -</section> - -<section class="oe_container"> - <div class="oe_row oe_spaced"> - <h2 class="oe_slogan">Motivate with Badges</h2> - <div class="oe_span6"> - <div class="oe_row_img oe_centered"> - <img class="oe_picture" src="crm_linkedin.png"> - </div> - </div> - <div class="oe_span6"> - <p class="oe_mt32"> -Inspire achievement with recognition of coworker's good work by rewarding badges. These can be deserved manually or upon completion of challenges. Add fun to the competition with rare badges. - </p> - </div> - </div> -</section> - -<section class="oe_container oe_dark"> - <div class="oe_row oe_spaced"> - <h2 class="oe_slogan">Adapt to any module</h2> - <div class="oe_span6"> - <p class="oe_mt32"> -Create goals linked to any module. The evaluation system is very flexible and can be used for many different tasks : sales evaluation, creation of events, project completion or even helping new users to complete their profile. - </p> - </div> - <div class="oe_span6"> - <div class="oe_row_img oe_centered"> - <img class="oe_picture oe_screenshot" src="crm_sc_02.png"> - </div> - </div> - </div> -</section> diff --git a/addons/gamification/i18n.moved/gamification.pot b/addons/gamification/i18n.moved/gamification.pot new file mode 100644 index 00000000000..0ef4cdbea64 --- /dev/null +++ b/addons/gamification/i18n.moved/gamification.pot @@ -0,0 +1,1733 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * gamification +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 8.0alpha1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-02-13 15:08+0000\n" +"PO-Revision-Date: 2014-02-13 15:08+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Search Goals" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_company_data +msgid "Set your Company Data" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Never" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,computation_mode:0 +msgid "Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal:0 +#: field:gamification.goal.wizard,goal_id:0 +msgid "Goal" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +msgid "Non recurring" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,next_report_date:0 +msgid "Next Report Date" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,monetary:0 +msgid "Monetary Value" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "My Goals" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,condition:0 +msgid "The lower the better" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: view:gamification.goal.wizard:0 +msgid "Grant Badge To" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_count_distinct:0 +msgid "The number of time this badge has been received by unique users." +msgstr "" + +#. module: gamification +#: help:gamification.badge,unique_owner_ids:0 +msgid "The list of unique users having received this badge." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal:0 +#: view:gamification.goal.definition:0 +msgid "Group By..." +msgstr "" + +#. module: gamification +#: model:gamification.challenge,name:gamification.challenge_base_configure +msgid "Setup your Company" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "/∞" +msgstr "" + +#. module: gamification +#: code:addons/gamification/wizard/grant_badge.py:44 +#, python-format +msgid "You can not grant a badge to yourself" +msgstr "" + +#. module: gamification +#: field:gamification.goal,completeness:0 +msgid "Completeness" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,field_date_id:0 +msgid "Date Field" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_my_this_month:0 +msgid "The number of time the current user has received this badge this month." +msgstr "" + +#. module: gamification +#: selection:gamification.challenge.line,condition:0 +msgid ">=" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,name:gamification.challenge_wizard +msgid "Challenge Description" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_badge_user_wizard +msgid "gamification.badge.user.wizard" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Reward" +msgstr "" + +#. module: gamification +#: help:gamification.badge,message_ids:0 +#: help:gamification.challenge,message_ids:0 +#: help:gamification.goal,message_ids:0 +msgid "Messages and communication history" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,category:0 +msgid "Define the visibility of the challenge through menus" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Refresh Challenge" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_challenge_line +msgid "Gamification generic goal for challenge" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "e.g. result = pool.get('mail.followers').search(cr, uid, [('res_model', '=', 'mail.group'), ('partner_id', '=', object.user_id.partner_id.id)], count=True, context=context)" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_monetary:0 +msgid "Monetary" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "In progress (to update)" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_goal_definition +msgid "Gamification goal definition" +msgstr "" + +#. module: gamification +#: help:gamification.badge,remaining_sending:0 +msgid "If a maxium is set" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,invited_user_ids:0 +msgid "Suggest to users" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: model:ir.actions.act_window,name:gamification.goal_list_action +#: model:ir.ui.menu,name:gamification.gamification_goal_menu +msgid "Goals" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Badge Description" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "than the target." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed." +msgstr "" + +#. module: gamification +#: help:gamification.challenge,manager_id:0 +msgid "The user responsible for the challenge." +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/goal.py:385 +#, python-format +msgid "Update %s" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,description:0 +msgid "Goal Description" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "this month" +msgstr "" + +#. module: gamification +#: field:gamification.badge,owner_ids:0 +msgid "Owners" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,sender_id:0 +msgid "Sender" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_my_this_month:0 +msgid "My Monthly Total" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,remind_update_delay:0 +msgid "Never reminded if no value or zero is specified." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: field:gamification.challenge.line,challenge_id:0 +#: field:gamification.goal,challenge_id:0 +msgid "Challenge" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,condition:0 +msgid "Goal Performance" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_ids:0 +#: field:gamification.challenge,message_ids:0 +#: field:gamification.goal,message_ids:0 +msgid "Messages" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "badges this month" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:517 +#: code:addons/gamification/models/goal.py:351 +#, python-format +msgid "Error!" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,line_ids:0 +msgid "Lines" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user:0 +msgid "Granted by" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_count_distinct:0 +msgid "Number of users" +msgstr "" + +#. module: gamification +#: help:gamification.badge,message_unread:0 +#: help:gamification.challenge,message_unread:0 +#: help:gamification.goal,message_unread:0 +msgid "If checked new messages require your attention." +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,name:gamification.action_new_simplified_res_users +msgid "Create User" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Yearly" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "People having some badges" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "granted," +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_full_suffix:0 +#: field:gamification.goal,definition_suffix:0 +#: field:gamification.goal.definition,suffix:0 +msgid "Suffix" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Reference" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,autojoin_group_id:0 +msgid "Auto-subscription Group" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "refresh" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Even if the challenge is failed, best challengers will be rewarded" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_id:0 +msgid "For Every Succeding User" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,target_goal:0 +msgid "Target Value to Reach" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,start_date:0 +msgid "The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Notification Messages" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,manager_id:0 +msgid "Responsible" +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:108 +#, python-format +msgid "more details..." +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,name:gamification.badge_list_action +#: model:ir.ui.menu,name:gamification.gamification_badge_menu +msgid "Badges" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_auth_badge_ids:0 +msgid "Only the people having these badges can give this badge" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Recorded manually" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Statistics" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "From" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Granting" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Grant" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "e.g. days" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_id:0 +#: view:gamification.goal:0 +#: field:gamification.goal,definition_id:0 +#: field:gamification.goal.definition,name:0 +msgid "Goal Definition" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,sequence:0 +msgid "Sequence" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "Failed" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: field:gamification.challenge,state:0 +#: view:gamification.goal:0 +#: field:gamification.goal,state:0 +msgid "State" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_follower_ids:0 +#: field:gamification.challenge,message_follower_ids:0 +#: field:gamification.goal,message_follower_ids:0 +msgid "Followers" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_first_id:0 +msgid "For 1st user" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Can not grant" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_unread:0 +#: field:gamification.challenge,message_unread:0 +#: field:gamification.goal,message_unread:0 +msgid "Unread Messages" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,display_mode:0 +msgid "Displayed as" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +#: field:gamification.badge,name:0 +#: field:gamification.badge.user,badge_id:0 +#: field:gamification.badge.user.wizard,badge_id:0 +msgid "Badge" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,suffix:0 +msgid "The unit of the target and current values" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: model:ir.actions.act_window,name:gamification.goals_from_challenge_act +msgid "Related Goals" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Subscriptions" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: model:ir.actions.act_window,name:gamification.action_grant_wizard +msgid "Grant Badge" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:683 +#, python-format +msgid "<br/>Reward (badge %s) for every succeeding user was sent to %s." +msgstr "" + +#. module: gamification +#: help:gamification.badge,message_summary:0 +#: help:gamification.challenge,message_summary:0 +#: help:gamification.goal,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: gamification +#: field:gamification.challenge,report_message_group_id:0 +msgid "Send a copy to" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.challenge_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a challenge. \n" +" </p>\n" +" <p>\n" +" Assign a list of goals to chosen users to evaluate them.\n" +" The challenge can use a period (weekly, monthly...) for automatic creation of goals.\n" +" The goals are created for the specified users or member of the group.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:695 +#, python-format +msgid "Nobody reached the required conditions to receive special badges." +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:35 +#: code:addons/gamification/static/src/xml/gamification.xml:57 +#, python-format +msgid "Target: <=" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Reminders for Manual Goals" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/goal.py:265 +#, python-format +msgid "Invalid return content from the evaluation of %s" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +#: field:gamification.goal.definition,computation_mode:0 +msgid "Computation Mode" +msgstr "" + +#. module: gamification +#: model:email.template,body_html:gamification.simple_report_template +msgid "<header><strong>${object.name}</strong>\n" +"</header><p class=\"oe_grey\">The following message contains the current progress for the challenge ${object.name}</p>\n" +"\n" +"% if object.visibility_mode == 'personal':\n" +" <table width=\"100%\" border=\"1\"><tr><th>Goal</th>\n" +" <th>Target</th>\n" +" <th>Current</th>\n" +" <th>Completeness</th>\n" +" </tr>\n" +" % for line in ctx[\"challenge_lines\"]:\n" +" <tr if line>= 100:\n" +" style=\"font-weight:bold;\"\n" +" % endif\n" +" >\n" +" <td>${line['name']}</td>\n" +" <td>${line['target']}\n" +" % if line['suffix']:\n" +" ${line['suffix']}\n" +" % endif\n" +" </td>\n" +" <td>${line['current']}\n" +" % if line['suffix']:\n" +" ${line['suffix']}\n" +" % endif\n" +" </td>\n" +" <td>${line['completeness']} %</td>\n" +" </tr>\n" +" % endfor\n" +" </table>\n" +"% else:\n" +" % for line in ctx[\"challenge_lines\"]:\n" +" <table width=\"100%\" border=\"1\"><tr><th colspan=\"4\">${line['name']}</th>\n" +" </tr><tr><th>#</th>\n" +" <th>Person</th>\n" +" <th>Completeness</th>\n" +" <th>Current</th>\n" +" </tr>\n" +" % for goal in line['goals']:\n" +" <tr if goal.completeness>= 100:\n" +" style=\"font-weight:bold;\"\n" +" % endif\n" +" >\n" +" <td>${goal['rank']}</td>\n" +" <td>${goal['name']}</td>\n" +" <td>${goal['completeness']}%</td>\n" +" <td>${goal['current']}/${line['target']}\n" +" % if line['suffix']:\n" +" ${line['suffix']}\n" +" % endif\n" +" </td>\n" +" </tr>\n" +" % endfor\n" +" </table><br><br>\n" +"\n" +" % endfor\n" +"% endif\n" +" " +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,condition:0 +msgid "A goal is considered as completed when the current value is compared to the value to reach" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,remind_update_delay:0 +msgid "Non-updated manual goals will be reminded after" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,full_suffix:0 +msgid "The currency and suffix field" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,display_mode:0 +msgid "Exclusive (done or not-done)" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:223 +#: code:addons/gamification/models/badge.py:225 +#: code:addons/gamification/models/badge.py:227 +#: code:addons/gamification/models/badge.py:229 +#: code:addons/gamification/wizard/grant_badge.py:44 +#, python-format +msgid "Warning!" +msgstr "" + +#. module: gamification +#: field:gamification.goal,last_update:0 +msgid "Last Update" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,report_message_frequency:0 +msgid "Report Frequency" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,field_id:0 +msgid "The field containing the value to evaluate" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,end_date:0 +msgid "The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date." +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,create_date:0 +msgid "Created" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_idea +msgid "Brilliant" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,visibility_mode:0 +msgid "Individual Goals" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Invited" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,report_message_frequency:0 +msgid "On change" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Daily" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal.definition:0 +msgid "Goal definitions" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Participating" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_auth:0 +msgid "Who can grant this badge" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,last_report_date:0 +msgid "Last Report Date" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Automatic: execute a specific Python code" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_goal_wizard +msgid "gamification.goal.wizard" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_badge_user +msgid "Gamification user badge" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Describe the challenge: what is does, who it targets, why it matters..." +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Goal List" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,comment:0 +#: field:gamification.badge.user.wizard,comment:0 +msgid "Comment" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "In progress" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Search Challenges" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "No one, assigned through challenges" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_my:0 +msgid "My Total" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "reply later" +msgstr "" + +#. module: gamification +#: field:gamification.badge,image:0 +msgid "Image" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Running Challenges" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,category:0 +msgid "Appears in" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Passed" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "Clickable Goals" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "Canceled" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,badge_name:0 +msgid "Badge Name" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,definition_suffix:0 +msgid "Unit" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:229 +#, python-format +msgid "You have already sent this badge too many time this month." +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_my_monthly_sending:0 +msgid "My Monthly Sending Total" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Check Badge" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Weekly" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Assign Challenge To" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: view:gamification.challenge:0 +#: view:gamification.goal.wizard:0 +msgid "or" +msgstr "" + +#. module: gamification +#: help:gamification.challenge.line,sequence:0 +msgid "Sequence number for ordering" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Category" +msgstr "" + +#. module: gamification +#: field:gamification.goal,current:0 +msgid "Current Value" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,field_date_id:0 +msgid "The date to use for the time period evaluated" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.goals_from_challenge_act +msgid "<p>\n" +" There is no goals associated to this challenge matching your search.\n" +" Make sure that your challenge is active and assigned to at least one user.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.badge_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a badge. \n" +" </p>\n" +" <p>\n" +" A badge is a symbolic token granted to a user as a sign of reward.\n" +" It can be deserved automatically when some conditions are met or manually by users.\n" +" Some badges are harder than others to get with specific conditions.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_auth_badge_ids:0 +msgid "Required Badges" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Line List" +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_max:0 +msgid "Monthly Limited Sending" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:517 +#, python-format +msgid "Retrieving progress for personal challenge without user information" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_timezone +msgid "Set your Timezone" +msgstr "" + +#. module: gamification +#: help:gamification.badge,goal_definition_ids:0 +msgid "The users that have succeeded theses goals will receive automatically the badge." +msgstr "" + +#. module: gamification +#: help:gamification.badge,image:0 +msgid "This field holds the image used for the badge, limited to 256x256" +msgstr "" + +#. module: gamification +#: view:gamification.goal.wizard:0 +msgid "Update" +msgstr "" + +#. module: gamification +#: field:gamification.goal,line_id:0 +msgid "Goal Line" +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_max_number:0 +msgid "Limitation Number" +msgstr "" + +#. module: gamification +#: model:ir.module.category,name:gamification.module_goal_category +msgid "Gamification" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,state:0 +#: view:gamification.goal:0 +#: selection:gamification.goal,state:0 +msgid "Draft" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Reject" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,model_id:0 +msgid "The model object for the field to evaluate" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.goal_definition_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a goal definition. \n" +" </p>\n" +" <p>\n" +" A goal definition is a technical model of goal defining a condition to reach.\n" +" The dates, values to reach or users are defined in goal instance.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: help:gamification.badge.user,sender_id:0 +msgid "The user who has send the badge" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_count:0 +msgid "The number of time this badge has been received." +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +#: field:gamification.goal.definition,model_id:0 +msgid "Model" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_challenge +msgid "Gamification challenge" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_max:0 +msgid "Check to set a monthly limit per person of sending this badge" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_badge +msgid "Gamification badge" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.action_new_simplified_res_users +msgid "Create and manage users that will connect to the system. Users can be deactivated should there be a period of time during which they will/should not connect to the system. You can assign them groups in order to give them specific access to the applications they need to use in the system." +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Security rules to define who is allowed to manually grant badges. Not enforced for administrator." +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,display_mode:0 +msgid "Progressive (using numerical values)" +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_gamification_goal +msgid "Gamification goal instance" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:685 +#, python-format +msgid "<br/>Nobody has succeeded to reach every goal, no badge is rewared for this challenge." +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_is_follower:0 +#: field:gamification.challenge,message_is_follower:0 +#: field:gamification.goal,message_is_follower:0 +msgid "Is a Follower" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,full_suffix:0 +msgid "Full Suffix" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Advanced Options" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,start_date:0 +#: field:gamification.goal,start_date:0 +msgid "Start Date" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_second_id:0 +msgid "For 2nd user" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Data" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,condition:0 +msgid "Condition" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,user_id:0 +#: field:gamification.badge.user.wizard,user_id:0 +#: view:gamification.goal:0 +#: field:gamification.goal,user_id:0 +msgid "User" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,domain:0 +msgid "Filter Domain" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Schedule" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Automatic: number of records" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_max_number:0 +msgid "The maximum number of time this badge can be sent per month per person." +msgstr "" + +#. module: gamification +#: model:ir.model,name:gamification.model_res_groups +msgid "Access Groups" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "Formating Options" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,compute_code:0 +msgid "Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id." +msgstr "" + +#. module: gamification +#: field:gamification.challenge,user_ids:0 +#: model:ir.model,name:gamification.model_res_users +msgid "Users" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "A selected list of users" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.challenge.line:0 +msgid "Challenge Lines" +msgstr "" + +#. module: gamification +#: help:gamification.badge,rule_auth_user_ids:0 +msgid "Only these people can give this badge" +msgstr "" + +#. module: gamification +#: field:gamification.badge,active:0 +msgid "Active" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,action_id:0 +msgid "The action that will be called to update the goal value." +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "You can still grant" +msgstr "" + +#. module: gamification +#: field:gamification.goal,computation_mode:0 +msgid "Computation mode" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,action_id:0 +msgid "Action" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_invite +msgid "Invite new Users" +msgstr "" + +#. module: gamification +#: field:gamification.goal,definition_description:0 +msgid "Definition Description" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_problem_solver +msgid "Problem Solver" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Target: less than" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_this_month:0 +msgid "The number of time this badge has been received this month." +msgstr "" + +#. module: gamification +#: help:gamification.goal,last_update:0 +msgid "In case of manual goal, reminders are sent if the goal as not been updated for a while (defined in challenge). Ignored in case of non-manual goal or goal not linked to a challenge." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Depending on the Display mode, reports will be individual or shared." +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Start goal" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,period:0 +msgid "Periodicity" +msgstr "" + +#. module: gamification +#: field:gamification.badge.user,create_uid:0 +msgid "Creator" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:692 +#, python-format +msgid "<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,compute_code:0 +msgid "Python Code" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Rewards for challenges" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Send Report" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Period" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,name:0 +msgid "Challenge Name" +msgstr "" + +#. module: gamification +#: model:ir.ui.menu,name:gamification.gamification_menu +msgid "Gamification Tools" +msgstr "" + +#. module: gamification +#: field:gamification.badge,goal_definition_ids:0 +msgid "Rewarded by" +msgstr "" + +#. module: gamification +#: field:gamification.badge,remaining_sending:0 +msgid "Remaining Sending Allowed" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,state:0 +msgid "Done" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,visibility_mode:0 +#: field:gamification.goal,definition_display:0 +msgid "Display Mode" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +#: view:gamification.goal.wizard:0 +msgid "Cancel" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "There is no reward upon completion of this challenge." +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,monetary:0 +msgid "The target and current value are defined in the company currency." +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,condition:0 +msgid "The higher the better" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_base_company_logo +msgid "Set your Company Logo" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,state:0 +msgid "In Progress" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Goal Reached" +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_my:0 +msgid "The number of time the current user has received this badge." +msgstr "" + +#. module: gamification +#: help:gamification.challenge,report_message_group_id:0 +msgid "Group that will receive a copy of the report in addition to the user" +msgstr "" + +#. module: gamification +#: field:gamification.goal,target_goal:0 +msgid "To Reach" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,res_id_field:0 +msgid "The field name on the user profile (res.users) containing the value for res_id for action." +msgstr "" + +#. module: gamification +#: help:gamification.goal,remind_update_delay:0 +msgid "The number of days after which the user assigned to a manual goal will be reminded. Never reminded if no value is specified." +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +msgid "Who would you like to reward?" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge.line,condition:0 +msgid "<=" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Configure Challenge" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,period:0 +#: selection:gamification.challenge,report_message_frequency:0 +msgid "Monthly" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,user_ids:0 +msgid "List of users participating to the challenge" +msgstr "" + +#. module: gamification +#: model:email.template,body_html:gamification.email_template_goal_reminder +msgid "<header><strong>Reminder ${object.name}</strong>\n" +" </header><p>You have not updated your progress for the goal ${object.definition_id.name} (currently reached at ${object.completeness}%) for at least ${object.remind_update_delay} days. Do not forget to do it.</p>\n" +"\n" +" <p>If you have not changed your score yet, you can use the button \"The current value is up to date\" to indicate so.</p>" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +#: field:gamification.goal.wizard,current:0 +msgid "Current" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "To" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:225 +#, python-format +msgid "You are not in the user allowed list." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Ã" +msgstr "" + +#. module: gamification +#: field:gamification.badge,challenge_ids:0 +msgid "Reward of Challenges" +msgstr "" + +#. module: gamification +#: field:gamification.goal,definition_condition:0 +msgid "Definition Condition" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,autojoin_group_id:0 +msgid "Group of users whose members will be automatically added to user_ids once the challenge is started" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +#: model:ir.actions.act_window,name:gamification.goal_definition_list_action +#: model:ir.ui.menu,name:gamification.gamification_definition_menu +msgid "Goal Definitions" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_hidden +msgid "Hidden" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: view:gamification.goal:0 +msgid "days" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,report_template_id:0 +msgid "Report Template" +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_auth:0 +msgid "Allowance to Grant" +msgstr "" + +#. module: gamification +#: selection:gamification.goal.definition,computation_mode:0 +msgid "Automatic: sum on a field" +msgstr "" + +#. module: gamification +#: model:gamification.challenge,name:gamification.challenge_base_discover +msgid "Complete your Profile" +msgstr "" + +#. module: gamification +#: field:gamification.badge,description:0 +#: field:gamification.challenge,description:0 +msgid "Description" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:223 +#, python-format +msgid "This badge can not be sent by users." +msgstr "" + +#. module: gamification +#: model:email.template,body_html:gamification.email_template_badge_received +msgid "<p>Congratulation, you have received the badge <strong>${object.badge_id.name}</strong> !\n" +" % if object.sender_id\n" +" This badge was granted by <strong>${object.sender_id.name}</strong>.\n" +" % endif\n" +" </p>\n" +"\n" +" % if object.comment\n" +" <p><em>${object.comment}</em></p>\n" +" % endif\n" +" " +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_this_month:0 +msgid "Monthly total" +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_third_id:0 +msgid "For 3rd user" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user.wizard:0 +msgid "Describe what they did and why it matters (will be public)" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/badge.py:227 +#, python-format +msgid "You do not have the required badges." +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "e.g. Monthly Sales Objectives" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,line_ids:0 +msgid "List of goals that will be set" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Badge List" +msgstr "" + +#. module: gamification +#: selection:gamification.challenge,visibility_mode:0 +msgid "Leader Board (Group Ranking)" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Reached when current value is" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/goal.py:351 +#, python-format +msgid "Can not modify the configuration of a started goal" +msgstr "" + +#. module: gamification +#: field:gamification.badge,unique_owner_ids:0 +msgid "Unique Owners" +msgstr "" + +#. module: gamification +#: code:addons/gamification/models/challenge.py:665 +#, python-format +msgid "The challenge %s is finished." +msgstr "" + +#. module: gamification +#: field:gamification.challenge,end_date:0 +#: view:gamification.goal:0 +#: field:gamification.goal,end_date:0 +msgid "End Date" +msgstr "" + +#. module: gamification +#: help:gamification.badge,owner_ids:0 +msgid "The list of instances of this badge granted to users" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Goal Failed" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,res_id_field:0 +msgid "ID Field of user" +msgstr "" + +#. module: gamification +#: field:gamification.challenge.line,name:0 +msgid "Name" +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:105 +#, python-format +msgid "Invited Challenges" +msgstr "" + +#. module: gamification +#: view:gamification.goal:0 +msgid "Reset Completion" +msgstr "" + +#. module: gamification +#: help:gamification.goal.definition,domain:0 +msgid "Domain for filtering records. The rule can contain reference to 'user' that is a browse record of the current user, e.g. [('user_id', '=', user.id)]." +msgstr "" + +#. module: gamification +#: help:gamification.badge,stat_my_monthly_sending:0 +msgid "The number of time the current user has sent this badge this month." +msgstr "" + +#. module: gamification +#: help:gamification.goal,challenge_id:0 +msgid "Challenge that generated the goal, assign challenge to users to generate goals with a value in this field." +msgstr "" + +#. module: gamification +#: field:gamification.badge,rule_auth_user_ids:0 +msgid "Authorized Users" +msgstr "" + +#. module: gamification +#: model:ir.actions.act_window,help:gamification.goal_list_action +msgid "<p class=\"oe_view_nocontent_create\">\n" +" Click to create a goal. \n" +" </p>\n" +" <p>\n" +" A goal is defined by a user and a goal definition.\n" +" Goals can be created automatically by using challenges.\n" +" </p>\n" +" " +msgstr "" + +#. module: gamification +#: field:gamification.challenge,reward_failure:0 +msgid "Reward Bests if not Succeeded?" +msgstr "" + +#. module: gamification +#: field:gamification.goal.definition,field_id:0 +msgid "Field to Sum" +msgstr "" + +#. module: gamification +#: field:gamification.badge,message_summary:0 +#: field:gamification.challenge,message_summary:0 +#: field:gamification.goal,message_summary:0 +msgid "Summary" +msgstr "" + +#. module: gamification +#: model:gamification.badge,name:gamification.badge_good_job +msgid "Good Job" +msgstr "" + +#. module: gamification +#. openerp-web +#: code:addons/gamification/static/src/xml/gamification.xml:32 +#: code:addons/gamification/static/src/xml/gamification.xml:54 +#, python-format +msgid "Target:" +msgstr "" + +#. module: gamification +#: view:gamification.goal.wizard:0 +msgid "Set the current value you have reached for this goal" +msgstr "" + +#. module: gamification +#: selection:gamification.badge,rule_auth:0 +msgid "Everyone" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "How to compute the goal?" +msgstr "" + +#. module: gamification +#: view:gamification.goal.definition:0 +msgid "Search Goal Definitions" +msgstr "" + +#. module: gamification +#: model:gamification.goal.definition,name:gamification.definition_nbr_following +msgid "Mail Group Following" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "Accept" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "Grant this Badge" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +#: model:ir.actions.act_window,name:gamification.challenge_list_action +#: model:ir.ui.menu,name:gamification.gamification_challenge_menu +msgid "Challenges" +msgstr "" + +#. module: gamification +#: model:res.groups,name:gamification.group_goal_manager +msgid "Manager" +msgstr "" + +#. module: gamification +#: view:gamification.challenge:0 +msgid "HR Challenges" +msgstr "" + +#. module: gamification +#: field:gamification.goal,remind_update_delay:0 +msgid "Remind delay" +msgstr "" + +#. module: gamification +#: view:gamification.badge:0 +msgid "No monthly sending limit" +msgstr "" + +#. module: gamification +#: selection:gamification.goal,state:0 +msgid "Reached" +msgstr "" + +#. module: gamification +#: view:gamification.badge.user:0 +msgid "the" +msgstr "" + +#. module: gamification +#: field:gamification.badge,stat_count:0 +msgid "Total" +msgstr "" + +#. module: gamification +#: help:gamification.challenge,period:0 +msgid "Period of automatic goal assigment. If none is selected, should be launched manually." +msgstr "" + diff --git a/addons/gamification/i18n/gamification.pot b/addons/gamification/i18n/gamification.pot index 0ef4cdbea64..03fcc645bec 100644 --- a/addons/gamification/i18n/gamification.pot +++ b/addons/gamification/i18n/gamification.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 8.0alpha1\n" +"Project-Id-Version: OpenERP Server 7.saas~3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-02-13 15:08+0000\n" -"PO-Revision-Date: 2014-02-13 15:08+0000\n" +"POT-Creation-Date: 2014-02-13 15:03+0000\n" +"PO-Revision-Date: 2014-02-13 15:03+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index fec8e04c4b7..06d0c44baac 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -433,7 +433,7 @@ class gamification_challenge(osv.Model): ##### JS utilities ##### def _get_serialized_challenge_lines(self, cr, uid, challenge, user_id=False, restrict_goal_ids=False, restrict_top=False, context=None): - """Return a serialised version of the goals information + """Return a serialised version of the goals information if the user has not completed every goal :challenge: browse record of challenge to compute :user_id: res.users id of the user retrieving progress (False if no distinction, only for ranking challenges) @@ -487,6 +487,7 @@ class gamification_challenge(osv.Model): (start_date, end_date) = start_end_date_for_period(challenge.period) res_lines = [] + all_reached = True for line in challenge.line_ids: line_data = { 'name': line.definition_id.name, @@ -537,6 +538,8 @@ class gamification_challenge(osv.Model): 'completeness': goal.completeness, 'state': goal.state, }) + if goal.state != 'reached': + all_reached = False else: ranking += 1 if user_id and goal.user_id.id == user_id: @@ -554,8 +557,12 @@ class gamification_challenge(osv.Model): 'completeness': goal.completeness, 'state': goal.state, }) + if goal.state != 'reached': + all_reached = False if goal_ids: res_lines.append(line_data) + if all_reached: + return [] return res_lines ##### Reporting ##### @@ -620,22 +627,28 @@ class gamification_challenge(osv.Model): return self.write(cr, uid, challenge.id, {'last_report_date': fields.date.today()}, context=context) ##### Challenges ##### + # TODO in trunk, remove unused parameter user_id def accept_challenge(self, cr, uid, challenge_ids, context=None, user_id=None): """The user accept the suggested challenge""" - user_id = user_id or uid + return self._accept_challenge(cr, uid, uid, challenge_ids, context=context) + + def _accept_challenge(self, cr, uid, user_id, challenge_ids, context=None): user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) message = "%s has joined the challenge" % user.name - self.message_post(cr, uid, challenge_ids, body=message, context=context) + self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context) self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': [(3, user_id)], 'user_ids': [(4, user_id)]}, context=context) - return self.generate_goals_from_challenge(cr, uid, challenge_ids, context=context) + return self.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) + # TODO in trunk, remove unused parameter user_id def discard_challenge(self, cr, uid, challenge_ids, context=None, user_id=None): """The user discard the suggested challenge""" - user_id = user_id or uid + return self._discard_challenge(cr, uid, uid, challenge_ids, context=context) + + def _discard_challenge(self, cr, uid, user_id, challenge_ids, context=None): user = self.pool.get('res.users').browse(cr, uid, user_id, context=context) message = "%s has refused the challenge" % user.name self.message_post(cr, SUPERUSER_ID, challenge_ids, body=message, context=context) - return self.write(cr, uid, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context) + return self.write(cr, SUPERUSER_ID, challenge_ids, {'invited_user_ids': (3, user_id)}, context=context) def reply_challenge_wizard(self, cr, uid, challenge_id, context=None): result = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'challenge_wizard') diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index d25d725a4ac..85cc18636f0 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -19,6 +19,7 @@ # ############################################################################## +from openerp import SUPERUSER_ID from openerp.osv import osv from challenge import MAX_VISIBILITY_RANKING @@ -39,10 +40,10 @@ class res_users_gamification_group(osv.Model): user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] challenge_obj = self.pool.get('gamification.challenge') - challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context) + challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', user_group_ids)], context=context) if challenge_ids: - challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context) - challenge_obj.generate_goals_from_challenge(cr, uid, challenge_ids, context=context) + challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context) + challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) return write_res def create(self, cr, uid, vals, context=None): @@ -54,10 +55,10 @@ class res_users_gamification_group(osv.Model): user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] challenge_obj = self.pool.get('gamification.challenge') - challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', user_group_ids)], context=context) + challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', user_group_ids)], context=context) if challenge_ids: - challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, write_res)]}, context=context) - challenge_obj.generate_goals_from_challenge(cr, uid, challenge_ids, context=context) + challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, write_res)]}, context=context) + challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) return write_res # def get_goals_todo_info(self, cr, uid, context=None): @@ -130,8 +131,8 @@ class res_groups_gamification_group(osv.Model): user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]] challenge_obj = self.pool.get('gamification.challenge') - challenge_ids = challenge_obj.search(cr, uid, [('autojoin_group_id', 'in', ids)], context=context) + challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', ids)], context=context) if challenge_ids: - challenge_obj.write(cr, uid, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context) - challenge_obj.generate_goals_from_challenge(cr, uid, challenge_ids, context=context) + challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context) + challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) return write_res diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index a47fd4637ee..734710be4d0 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -7,6 +7,7 @@ # 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 @@ -36,21 +37,185 @@ from dateutil import parser import pytz from openerp.osv import fields, osv from openerp.osv import osv +from collections import namedtuple + +import logging +_logger = logging.getLogger(__name__) + + +class Meta(type): + """ This Meta class allow to define class as a structure, and so instancied variable + in __init__ to avoid to have side effect alike 'static' variable """ + def __new__(typ, name, parents, attrs): + methods = dict((k, v) for k, v in attrs.iteritems() + if callable(v)) + attrs = dict((k, v) for k, v in attrs.iteritems() + if not callable(v)) + + def init(self, **kw): + for k, v in attrs.iteritems(): + setattr(self, k, v) + for k, v in kw.iteritems(): + assert k in attrs + setattr(self, k, v) + + methods['__init__'] = init + methods['__getitem__'] = getattr + return type.__new__(typ, name, parents, methods) + +class Struct(object): + __metaclass__ = Meta + +class OpenerpEvent(Struct): + event = False + found = False + event_id = False + isRecurrence = False + isInstance = False + update = False + status = False + attendee_id = False + synchro = False + +class GmailEvent(Struct): + event = False + found = False + isRecurrence = False + isInstance = False + update = False + status = False + +class SyncEvent(object): + def __init__(self): + self.OE = OpenerpEvent() + self.GG = GmailEvent() + self.OP = None + + def __getitem__(self, key): + return getattr(self,key) + + def compute_OP(self): + #If event are already in Gmail and in OpenERP + if self.OE.found and self.GG.found: + #If the event has been deleted from one side, we delete on other side ! + if self.OE.status != self.GG.status: + self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"), + 'The event has been deleted from one side, we delete on other side !' ) + #If event is not deleted ! + elif self.OE.status and self.GG.status: + if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]: + if self.OE.update < self.GG.update: + tmpSrc = 'GG' + elif self.OE.update > self.GG.update: + tmpSrc = 'OE' + assert tmpSrc in ['GG','OE'] + + + #if self.OP.action == None: + if self[tmpSrc].isRecurrence: + if self[tmpSrc].status: + self.OP = Update(tmpSrc, 'Only need to update, because i\'m active') + else: + self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence') + + elif self[tmpSrc].isInstance: + self.OP= Update(tmpSrc, 'Only need to update, because already an exclu'); + else: + self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event'); + #end-if self.OP.action == None: + + else: + if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]: + self.OP = Update('OE','Event already updated by another user, but not synchro with my google calendar') + #import ipdb; ipdb.set_trace(); + else: + self.OP = NothingToDo("",'Not update needed') + else: + self.OP = NothingToDo("", "Both are already deleted"); + + # New in openERP... Create on create_events of synchronize function + elif self.OE.found and not self.GG.found: + #Has been deleted from gmail + if self.OE.status: + self.OP = Delete('OE', 'Removed from GOOGLE') + else: + self.OP = NothingToDo("","Already Deleted in gmail and unlinked in OpenERP") + elif self.GG.found and not self.OE.found: + tmpSrc = 'GG' + if not self.GG.status and not self.GG.isInstance: + # don't need to make something... because event has been created and deleted before the synchronization + self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly') + else: + if self.GG.isInstance: + if self[tmpSrc].status: + self.OP = Exclude(tmpSrc, 'Need to create the new exclu') + else: + self.OP = Exclude(tmpSrc, 'Need to copy and Exclude') + else: + self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL') + + def __str__(self): + return self.__repr__() + + def __repr__(self): + myPrint = "---- A SYNC EVENT ---" + myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id) + myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False)) + myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name) + myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False)) + myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found) + myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence) + myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance) + myPrint += "\n Synchro OE: %10s " % (self.OE.synchro) + myPrint += "\n Update OE: %10s " % (self.OE.update) + myPrint += "\n Update GG: %10s " % (self.GG.update) + myPrint += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status) + if (self.OP is None): + myPrint += "\n Action %s" % "---!!!---NONE---!!!---" + else: + myPrint += "\n Action %s" % type(self.OP).__name__ + myPrint += "\n Source %s" % (self.OP.src) + myPrint += "\n comment %s" % (self.OP.info) + return myPrint + + +class SyncOperation(object): + def __init__(self, src,info, **kw): + self.src = src + self.info = info + for k,v in kw.items(): + setattr(self,k,v) + + def __str__(self): + return 'in__STR__' + +class Create(SyncOperation): + pass +class Update(SyncOperation): + pass +class Delete(SyncOperation): + pass +class NothingToDo(SyncOperation): + pass +class Exclude(SyncOperation): + pass class google_calendar(osv.AbstractModel): STR_SERVICE = 'calendar' _name = 'google.%s' % STR_SERVICE - def generate_data(self, cr, uid, event, context=None): + def generate_data(self, cr, uid, event, context=None): if event.allday: start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) , context=context).isoformat('T').split('T')[0] end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0] type = 'date' + vstype = 'dateTime' else: start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') type = 'dateTime' + vstype = 'date' attendee_list = [] for attendee in event.attendee_ids: @@ -64,10 +229,12 @@ class google_calendar(osv.AbstractModel): "description": event.description or '', "start":{ type:start_date, + vstype:None, 'timeZone':'UTC' }, "end":{ type:end_date, + vstype:None, 'timeZone':'UTC' }, "attendees":attendee_list, @@ -80,10 +247,13 @@ class google_calendar(osv.AbstractModel): if not event.active: data["state"] = "cancelled" + if not self.get_need_synchro_attendee(cr,uid,context=context): + data.pop("attendees") + return data def create_an_event(self, cr, uid,event, context=None): - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] data = self.generate_data(cr, uid,event, context=context) @@ -94,7 +264,7 @@ class google_calendar(osv.AbstractModel): return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context) def delete_an_event(self, cr, uid,event_id, context=None): - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] params = { 'access_token' : self.get_token(cr,uid,context) @@ -108,12 +278,14 @@ class google_calendar(osv.AbstractModel): if not token: token = self.get_token(cr,uid,context) - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] params = { 'fields': 'items,nextPageToken', 'access_token' : token, - 'maxResults':1000 + 'maxResults':1000, + 'timeMin': self.get_start_time_to_synchro(cr,uid,context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"), + } headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} @@ -134,7 +306,7 @@ class google_calendar(osv.AbstractModel): def update_to_google(self, cr, uid, oe_event, google_event, context): calendar_event = self.pool['calendar.event'] - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'],'id,updated', self.get_token(cr,uid,context)) headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} @@ -148,10 +320,10 @@ class google_calendar(osv.AbstractModel): calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date':update_date}) if context['curr_attendee']: - self.pool.get('calendar.attendee').write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date},context) + self.pool['calendar.attendee'].write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date},context) def update_an_event(self, cr, uid,event, context=None): - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] data = self.generate_data(cr, uid,event, context=context) @@ -164,7 +336,7 @@ class google_calendar(osv.AbstractModel): return response def update_recurrent_event_exclu(self, cr, uid,instance_id,event_ori_google_id,event_new, context=None): - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] data = self.generate_data(cr, uid,event_new, context=context) @@ -186,7 +358,7 @@ class google_calendar(osv.AbstractModel): calendar_event = self.pool['calendar.event'] res_partner_obj = self.pool['res.partner'] calendar_attendee_obj = self.pool['calendar.attendee'] - user_obj = self.pool.get('res.users') + user_obj = self.pool['res.users'] myPartnerID = user_obj.browse(cr,uid,uid,context).partner_id.id attendee_record = [] partner_record = [(4,myPartnerID)] @@ -203,14 +375,16 @@ class google_calendar(osv.AbstractModel): if google_attendee.get('found',False): continue - attendee_id = res_partner_obj.search(cr, uid,[('email', '=', google_attendee['email'])], context=context) - if not attendee_id: - attendee_id = [res_partner_obj.create(cr, uid,{'email': google_attendee['email'],'Customer': False, 'name': google_attendee.get("displayName",False) or google_attendee['email'] }, context=context)] - attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) - partner_record.append((4, attendee.get('id'))) - attendee['partner_id'] = attendee.pop('id') - attendee['state'] = google_attendee['responseStatus'] - attendee_record.append((0, 0, attendee)) + if self.get_need_synchro_attendee(cr,uid,context=context): + attendee_id = res_partner_obj.search(cr, uid,[('email', '=', google_attendee['email'])], context=context) + if not attendee_id: + attendee_id = [res_partner_obj.create(cr, uid,{'email': google_attendee['email'],'customer': False, 'name': google_attendee.get("displayName",False) or google_attendee['email'] }, context=context)] + attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) + partner_record.append((4, attendee.get('id'))) + attendee['partner_id'] = attendee.pop('id') + attendee['state'] = google_attendee['responseStatus'] + attendee_record.append((0, 0, attendee)) + UTC = pytz.timezone('UTC') if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled if single_event_dict['start'].get('dateTime',False) and single_event_dict['end'].get('dateTime',False): @@ -244,7 +418,7 @@ class google_calendar(osv.AbstractModel): 'location':single_event_dict.get('location',False), 'class':single_event_dict.get('visibility','public'), 'oe_update_date':update_date, -# 'google_internal_event_id': single_event_dict.get('id',False), + # 'google_internal_event_id': single_event_dict.get('id',False), }) if single_event_dict.get("recurrence",False): @@ -261,16 +435,17 @@ class google_calendar(osv.AbstractModel): res = calendar_event.create(cr, uid, result, context=context) if context['curr_attendee']: - self.pool.get('calendar.attendee').write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date,'google_internal_event_id': single_event_dict.get('id',False)},context) + self.pool['calendar.attendee'].write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date,'google_internal_event_id': single_event_dict.get('id',False)},context) return res def synchronize_events(self, cr, uid, ids, context=None): - gc_obj = self.pool.get('google.calendar') + gc_obj = self.pool['google.calendar'] + # Create all new events from OpenERP into Gmail, if that is not recurrent event self.create_new_events(cr, uid, context=context) self.bind_recurring_events_to_google(cr, uid, context) - cr.commit() + res = self.update_events(cr, uid, context) @@ -279,10 +454,9 @@ class google_calendar(osv.AbstractModel): "url" : '' } - def create_new_events(self, cr, uid, context): - gc_pool = self.pool.get('google.calendar') - - calendar_event = self.pool['calendar.event'] + def create_new_events(self, cr, uid, context=None): + gc_pool = self.pool['google.calendar'] + ev_obj = self.pool['calendar.event'] att_obj = self.pool['calendar.attendee'] user_obj = self.pool['res.users'] myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id @@ -290,75 +464,73 @@ class google_calendar(osv.AbstractModel): context_norecurrent = context.copy() context_norecurrent['virtual_id'] = False - my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent) + my_att_ids = att_obj.search(cr, uid,[ ('partner_id', '=', myPartnerID), + ('google_internal_event_id', '=', False), + '|', + ('event_id.date_deadline','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), + ('event_id.end_date','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), + ], context=context_norecurrent) + for att in att_obj.browse(cr,uid,my_att_ids,context=context): if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: response = self.create_an_event(cr,uid,att.event_id,context=context) update_date = datetime.strptime(response['updated'],"%Y-%m-%dT%H:%M:%S.%fz") - calendar_event.write(cr, uid, att.event_id.id, {'oe_update_date':update_date}) + ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date':update_date}) att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date':update_date}) - cr.commit() - return True + cr.commit() - def get_empty_synchro_summarize(self) : - return { - #OPENERP - 'OE_event' : False, - 'OE_found' : False, - 'OE_event_id' : False, - 'OE_isRecurrence':False, - 'OE_isInstance':False, - 'OE_update':False, - 'OE_status':False, - 'OE_attendee_id': False, - 'OE_synchro':False, - - #GOOGLE - 'GG_event' : False, - 'GG_found' : False, - 'GG_isRecurrence':False, - 'GG_isInstance':False, - 'GG_update':False, - 'GG_status':False, - - #TO_DO_IN_GOOGLE - 'td_action':'', # create, update, delete, None - #If 'td_action' in (create , update), - # If td_source == OE - # We create in google the event based on OpenERP - # If td_source == GG - # We create in OpenERP the event based on Gmail - # - #If 'td_action' in (delete), - # If td_source == OE - # We delete in OpenERP the event - # If td_source == GG - # We delete in Gmail the event - # If td_source == ALL - # We delete in openERP AND in Gmail the event - 'td_source': '', # OE, GG, ALL - 'td_comment':'' + def bind_recurring_events_to_google(self, cr, uid, context): + ev_obj = self.pool['calendar.event'] + att_obj = self.pool['calendar.attendee'] + user_obj = self.pool['res.users'] + myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id - } + context_norecurrent = context.copy() + context_norecurrent['virtual_id'] = False + context_norecurrent['active_test'] = False - def update_events(self, cr, uid, context): + my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent) + + for att in att_obj.browse(cr,uid,my_att_ids,context=context): + if att.event_id.recurrent_id and att.event_id.recurrent_id > 0: + new_google_internal_event_id = False + source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context) + source_attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',source_event_record.id)], context=context) + source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0] + + if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id: + new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.split(' ')[0].replace('-','') + elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id: + new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.replace('-','').replace(' ','T').replace(':','') + 'Z' + + if new_google_internal_event_id: + #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! + res = self.update_recurrent_event_exclu(cr, uid,new_google_internal_event_id, source_attendee_record.google_internal_event_id,att.event_id, context=context) + att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context) + cr.commit() + + def update_events(self, cr, uid, context=None): if context is None: context = {} calendar_event = self.pool['calendar.event'] user_obj = self.pool['res.users'] att_obj = self.pool['calendar.attendee'] - myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id + myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id context_novirtual = context.copy() context_novirtual['virtual_id'] = False context_novirtual['active_test'] = False - all_event_from_google = self.get_event_dict(cr,uid,context=context) - all_new_event_from_google = all_event_from_google.copy() - + all_event_from_google = self.get_event_dict(cr, uid, context=context) + # Select all events from OpenERP which have been already synchronized in gmail - my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '!=', False)], context=context_novirtual) + my_att_ids = att_obj.search(cr, uid,[ ('partner_id', '=', myPartnerID), + ('google_internal_event_id', '!=', False), + '|', + ('event_id.date_deadline','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), + ('event_id.end_date','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), + ], context=context_novirtual) event_to_synchronize = {} for att in att_obj.browse(cr,uid,my_att_ids,context=context): event = att.event_id @@ -369,18 +541,19 @@ class google_calendar(osv.AbstractModel): event_to_synchronize[base_event_id] = {} if att.google_internal_event_id not in event_to_synchronize[base_event_id]: - event_to_synchronize[base_event_id][att.google_internal_event_id] = self.get_empty_synchro_summarize() - - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_attendee_id'] = att.id - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_event'] = event - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_found'] = True - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_event_id'] = event.id - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_isRecurrence'] = event.recurrency - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_isInstance'] = bool(event.recurrent_id and event.recurrent_id > 0) - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_update'] = event.oe_update_date - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_status'] = event.active - event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_synchro'] = att.oe_synchro_date - + event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent() + + ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id] + + ev_to_sync.OE.attendee_id = att.id + ev_to_sync.OE.event = event + ev_to_sync.OE.found = True + ev_to_sync.OE.event_id = event.id + ev_to_sync.OE.isRecurrence = event.recurrency + ev_to_sync.OE.isInstance = bool(event.recurrent_id and event.recurrent_id > 0) + ev_to_sync.OE.update = event.oe_update_date + ev_to_sync.OE.status = event.active + ev_to_sync.OE.synchro = att.oe_synchro_date for event in all_event_from_google.values(): event_id = event.get('id') @@ -390,98 +563,28 @@ class google_calendar(osv.AbstractModel): event_to_synchronize[base_event_id] = {} if event_id not in event_to_synchronize[base_event_id]: - event_to_synchronize[base_event_id][event_id] = self.get_empty_synchro_summarize() - - event_to_synchronize[base_event_id][event_id]['GG_event'] = event - event_to_synchronize[base_event_id][event_id]['GG_found'] = True - event_to_synchronize[base_event_id][event_id]['GG_isRecurrence'] = bool(event.get('recurrence','')) - event_to_synchronize[base_event_id][event_id]['GG_isInstance'] = bool(event.get('recurringEventId',0)) - event_to_synchronize[base_event_id][event_id]['GG_update'] = event.get('updated',None) # if deleted, no date without browse event - if event_to_synchronize[base_event_id][event_id]['GG_update']: - event_to_synchronize[base_event_id][event_id]['GG_update'] =event_to_synchronize[base_event_id][event_id]['GG_update'].replace('T',' ').replace('Z','') - event_to_synchronize[base_event_id][event_id]['GG_status'] = (event.get('status') != 'cancelled') - - + event_to_synchronize[base_event_id][event_id] = SyncEvent() + + ev_to_sync = event_to_synchronize[base_event_id][event_id] + + ev_to_sync.GG.event = event + ev_to_sync.GG.found = True + ev_to_sync.GG.isRecurrence = bool(event.get('recurrence','')) + ev_to_sync.GG.isInstance = bool(event.get('recurringEventId',0)) + ev_to_sync.GG.update = event.get('updated',None) # if deleted, no date without browse event + if ev_to_sync.GG.update: + ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T',' ').replace('Z','') + ev_to_sync.GG.status = (event.get('status') != 'cancelled') + ###################### # PRE-PROCESSING # ###################### - for base_event in event_to_synchronize: for current_event in event_to_synchronize[base_event]: - event = event_to_synchronize[base_event][current_event] - - #If event are already in Gmail and in OpenERP - if event['OE_found'] and event['GG_found']: - #If the event has been deleted from one side, we delete on other side ! - if event['OE_status'] != event['GG_status']: - event['td_action'] = "DELETE" - event['td_source'] = (event['OE_status'] and "OE") or (event['GG_status'] and "GG") - #If event is not deleted ! - elif event['OE_status'] and event['GG_status']: - if event['OE_update'].split('.')[0] != event['GG_update'].split('.')[0]: - if event['OE_update'] < event['GG_update']: - event['td_source'] = 'GG' - elif event['OE_update'] > event['GG_update']: - event['td_source'] = 'OE' - - - if event['td_action'] != "None": - if event['%s_isRecurrence' % event['td_source']]: - if event['%s_status' % event['td_source']]: - event['td_action'] = "UPDATE" - event['td_comment'] = 'Only need to update, because i\'m active' - else: - event['td_action'] = "EXCLUDE" - event['td_comment'] = 'Need to Exclude (Me = First event from recurrence) from recurrence' - - elif event['%s_isInstance' % event['td_source']]: - event['td_action'] = "UPDATE" - event['td_comment'] = 'Only need to update, because already an exclu' - else: - event['td_action'] = "UPDATE" - event['td_comment'] = 'Simply Update... I\'m a single event' - - else: - if not event['OE_synchro'] or event['OE_synchro'].split('.')[0] < event['OE_update'].split('.')[0]: - event['td_source'] = 'OE' - event['td_action'] = "UPDATE" - event['td_comment'] = 'Event already updated by another user, but not synchro with my google calendar' - - else: - event['td_action'] = "None" - event['td_comment'] = 'Not update needed' - else: - event['td_action'] = "None" - event['td_comment'] = "Both are already deleted" - # New in openERP... Create on create_events of synchronize function - elif event['OE_found'] and not event['GG_found']: - #Has been deleted from gmail - if event['OE_status']: - event['td_source'] = 'OE' - event['td_action'] = 'DELETE' - event['td_comment'] = 'Removed from GOOGLE ?' - else: - event['td_action'] = "None" - event['td_comment'] = "Already Deleted in gmail and unlinked in OpenERP" - elif event['GG_found'] and not event['OE_found']: - event['td_source'] = 'GG' - if not event['GG_status'] and not event['GG_isInstance']: - # don't need to make something... because event has been created and deleted before the synchronization - event['td_action'] = 'None' - event['td_comment'] = 'Nothing to do... Create and Delete directly' - - else: - if event['GG_isInstance']: - if event['%s_status' % event['td_source']]: - event['td_action'] = "EXCLUDE" - event['td_comment'] = 'Need to create the new exclu' - else: - event['td_action'] = "EXCLUDE" - event['td_comment'] = 'Need to copy and Exclude' - else: - event['td_action'] = "CREATE" - event['td_comment'] = 'New EVENT CREATE from GMAIL' - + event_to_synchronize[base_event][current_event].compute_OP() + #print event_to_synchronize[base_event] + #print "========================================================" + ###################### # DO ACTION # ###################### @@ -489,105 +592,62 @@ class google_calendar(osv.AbstractModel): event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(),key=operator.itemgetter(0)) for current_event in event_to_synchronize[base_event]: cr.commit() - event = current_event[1] - ############# - ### DEBUG ### - ############# -# if event['td_action'] and event['td_action'] != 'None': -# print " Real Event %s (%s)" % (current_event[0],event['OE_event_id']) -# print " Found OE:%5s vs GG: %5s" % (event['OE_found'],event['GG_found']) -# print " Recurrence OE:%5s vs GG: %5s" % (event['OE_isRecurrence'],event['GG_isRecurrence']) -# print " Instance OE:%5s vs GG: %5s" % (event['OE_isInstance'],event['GG_isInstance']) -# print " Synchro OE: %10s " % (event['OE_synchro']) -# print " Update OE: %10s " % (event['OE_update']) -# print " Update GG: %10s " % (event['GG_update']) -# print " Status OE:%5s vs GG: %5s" % (event['OE_status'],event['GG_status']) -# print " Action %s" % (event['td_action']) -# print " Source %s" % (event['td_source']) -# print " comment %s" % (event['td_comment']) - - context['curr_attendee'] = event.get('OE_attendee_id',False) - - actToDo = event['td_action'] - actSrc = event['td_source'] - if not actToDo: - raise ("#!? WHAT I NEED TO DO ????") - else: - if actToDo == 'None': - continue - elif actToDo == 'CREATE': - context_tmp = context.copy() - context_tmp['NewMeeting'] = True - if actSrc == 'GG': - res = self.update_from_google(cr, uid, False, event['GG_event'], "create", context=context_tmp) - event['OE_event_id'] = res - meeting = calendar_event.browse(cr,uid,res,context=context) - attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',res)], context=context) - self.pool.get('calendar.attendee').write(cr,uid,attendee_record_id, {'oe_synchro_date':meeting.oe_update_date,'google_internal_event_id': event['GG_event']['id']},context=context_tmp) - elif actSrc == 'OE': - raise "Should be never here, creation for OE is done before update !" - #TODO Add to batch - elif actToDo == 'UPDATE': - if actSrc == 'GG': - self.update_from_google(cr, uid, event['OE_event'], event['GG_event'], 'write', context) - elif actSrc == 'OE': - self.update_to_google(cr, uid, event['OE_event'], event['GG_event'], context) - elif actToDo == 'EXCLUDE' : - if actSrc == 'OE': - self.delete_an_event(cr,uid,current_event[0],context=context) - elif actSrc == 'GG': - new_google_event_id = event['GG_event']['id'].split('_')[1] - if 'T' in new_google_event_id: - new_google_event_id = new_google_event_id.replace('T','')[:-1] - else: - new_google_event_id = new_google_event_id + "000000" - - if event['GG_status']: - parent_event = {} - parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].get('OE_event_id') , new_google_event_id) - res = self.update_from_google(cr, uid, parent_event, event['GG_event'], "copy", context) - else: - if event_to_synchronize[base_event][0][1].get('OE_event_id'): - parent_oe_id = event_to_synchronize[base_event][0][1].get('OE_event_id') - calendar_event.unlink(cr,uid,"%s-%s" % (parent_oe_id,new_google_event_id),unlink_level=1,context=context) - - elif actToDo == 'DELETE': - if actSrc == 'GG': - self.delete_an_event(cr,uid,current_event[0],context=context) - elif actSrc == 'OE': - calendar_event.unlink(cr,uid,event['OE_event_id'],unlink_level=0,context=context) - return True + event = current_event[1] # event is an Sync Event ! + + actToDo = event.OP + actSrc = event.OP.src - def bind_recurring_events_to_google(self, cr, uid, context): - calendar_event = self.pool['calendar.event'] - att_obj = self.pool.get('calendar.attendee') - user_obj = self.pool['res.users'] - myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id + # if not isinstance(actToDo, NothingToDo): + # print event + + context['curr_attendee'] = event.OE.attendee_id - context_norecurrent = context.copy() - context_norecurrent['virtual_id'] = False - context_norecurrent['active_test'] = False - - my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent) - for att in att_obj.browse(cr,uid,my_att_ids,context=context): - if att.event_id.recurrent_id and att.event_id.recurrent_id > 0: - new_google_internal_event_id = False - source_event_record = calendar_event.browse(cr, uid, att.event_id.recurrent_id, context) - source_attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',source_event_record.id)], context=context) - source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context) - if source_attendee_record: - source_attendee_record = source_attendee_record[0] - - if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id: - new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.split(' ')[0].replace('-','') - elif event.recurrent_id_date and source_attendee_record.google_internal_event_id: - new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.replace('-','').replace(' ','T').replace(':','') + 'Z' + if isinstance(actToDo, NothingToDo): + continue + elif isinstance(actToDo, Create): + context_tmp = context.copy() + context_tmp['NewMeeting'] = True + if actSrc == 'GG': + res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp) + event.OE.event_id = res + meeting = calendar_event.browse(cr,uid,res,context=context) + attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',res)], context=context) + self.pool['calendar.attendee'].write(cr, uid, attendee_record_id, {'oe_synchro_date':meeting.oe_update_date, 'google_internal_event_id':event.GG.event['id']}, context=context_tmp) + elif actSrc == 'OE': + raise "Should be never here, creation for OE is done before update !" + #TODO Add to batch + elif isinstance(actToDo, Update): + if actSrc == 'GG': + self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context) + elif actSrc == 'OE': + self.update_to_google(cr, uid, event.OE.event, event.GG.event, context) + elif isinstance(actToDo, Exclude): + if actSrc == 'OE': + self.delete_an_event(cr,uid,current_event[0],context=context) + elif actSrc == 'GG': + new_google_event_id = event.GG.event['id'].split('_')[1] + if 'T' in new_google_event_id: + new_google_event_id = new_google_event_id.replace('T','')[:-1] + else: + new_google_event_id = new_google_event_id + "000000" - if new_google_internal_event_id: - #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! - res = self.update_recurrent_event_exclu(cr,uid,new_google_internal_event_id,source_attendee_record.google_internal_event_id,att.event_id,context=context) - att_obj.write(cr, uid, [att.event_id.id], {'google_internal_event_id': new_google_internal_event_id}) + if event.GG.status: + parent_event = {} + parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id , new_google_event_id) + res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) + else: + if event_to_synchronize[base_event][0][1].OE.event_id: + parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id + calendar_event.unlink(cr,uid,"%s-%s" % (parent_oe_id,new_google_event_id),unlink_level=1,context=context) + + elif isinstance(actToDo, Delete): + if actSrc == 'GG': + self.delete_an_event(cr,uid,current_event[0],context=context) + elif actSrc == 'OE': + calendar_event.unlink(cr,uid,event.OE.event_id,unlink_level=0,context=context) + return True + def check_and_sync(self, cr, uid, oe_event, google_event, context): if datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"): self.update_to_google(cr, uid, oe_event, google_event, context) @@ -595,7 +655,7 @@ class google_calendar(osv.AbstractModel): self.update_from_google(cr, uid, oe_event, google_event, 'write', context) def get_sequence(self,cr,uid,instance_id,context=None): - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] params = { 'fields': 'sequence', @@ -613,7 +673,7 @@ class google_calendar(osv.AbstractModel): ################################# def get_token(self,cr,uid,context=None): - current_user = self.pool.get('res.users').browse(cr,uid,uid,context=context) + current_user = self.pool['res.users'].browse(cr,uid,uid,context=context) if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)): self.do_refresh_token(cr,uid,context=context) @@ -622,20 +682,20 @@ class google_calendar(osv.AbstractModel): return current_user.google_calendar_token def do_refresh_token(self,cr,uid,context=None): - current_user = self.pool.get('res.users').browse(cr,uid,uid,context=context) - gs_pool = self.pool.get('google.service') + current_user = self.pool['res.users'].browse(cr,uid,uid,context=context) + gs_pool = self.pool['google.service'] refresh = current_user.google_calendar_rtoken - all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken,self.STR_SERVICE,context=context) + all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context) vals = {} vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') - self.pool.get('res.users').write(cr,SUPERUSER_ID,uid,vals,context=context) + self.pool['res.users'].write(cr,SUPERUSER_ID,uid,vals,context=context) def need_authorize(self,cr,uid,context=None): - current_user = self.pool.get('res.users').browse(cr,uid,uid,context=context) + current_user = self.pool['res.users'].browse(cr,uid,uid,context=context) return current_user.google_calendar_rtoken == False def get_calendar_scope(self,RO=False): @@ -643,22 +703,30 @@ class google_calendar(osv.AbstractModel): return 'https://www.googleapis.com/auth/calendar%s' % (readonly) def authorize_google_uri(self,cr,uid,from_url='http://www.openerp.com',context=None): - url = self.pool.get('google.service')._get_authorize_uri(cr,uid,from_url,self.STR_SERVICE,scope=self.get_calendar_scope(),context=context) + url = self.pool['google.service']._get_authorize_uri(cr,uid,from_url,self.STR_SERVICE,scope=self.get_calendar_scope(),context=context) return url def can_authorize_google(self,cr,uid,context=None): return self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager') def set_all_tokens(self,cr,uid,authorization_code,context=None): - gs_pool = self.pool.get('google.service') + gs_pool = self.pool['google.service'] all_token = gs_pool._get_google_token_json(cr, uid, authorization_code,self.STR_SERVICE,context=context) vals = {} vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token') vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') - self.pool.get('res.users').write(cr,SUPERUSER_ID,uid,vals,context=context) + self.pool['res.users'].write(cr,SUPERUSER_ID,uid,vals,context=context) + def get_start_time_to_synchro(self, cr, uid, context=None) : + # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 + number_of_week = 13 + return datetime.now()-timedelta(weeks=number_of_week) + + def get_need_synchro_attendee(self, cr, uid, context=None): + # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 + return True class res_users(osv.Model): _inherit = 'res.users' @@ -718,7 +786,6 @@ class calendar_attendee(osv.Model): # If attendees are updated, we need to specify that next synchro need an action # Except if it come from an update_from_google if not context.get('curr_attendee', False) and not context.get('NewMeeting', False): - self.pool.get('calendar.event').write(cr, uid, ref, {'oe_update_date':datetime.now()},context) - - return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context) + self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date':datetime.now()},context) + return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)
\ No newline at end of file diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index d87cc6314e9..d4b84bab012 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -74,8 +74,8 @@ <label for="number_of_days_temp" string="Duration"/> <div> <group col="3" attrs="{'invisible': [('type', '=', 'add')]}"> - <field name="date_from" nolabel="1" on_change="onchange_date_from(date_to, date_from)" required="1" class="oe_inline"/><label string="-" class="oe_inline"/> - <field name="date_to" nolabel="1" on_change="onchange_date_to(date_to, date_from)" required="1" class="oe_inline"/> + <field name="date_from" nolabel="1" on_change="onchange_date_from(date_to, date_from)" attrs="{'required':[('type', '=', 'remove')]}" class="oe_inline"/><label string="-" class="oe_inline"/> + <field name="date_to" nolabel="1" on_change="onchange_date_to(date_to, date_from)" attrs="{'required':[('type', '=', 'remove')]}" class="oe_inline"/> </group> <div> <field name="number_of_days_temp" class="oe_inline"/> days diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 23195857a9c..e950950a985 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -95,6 +95,9 @@ class EscposDriver(Thread): _logger.warning('ESC/POS Device Disconnected: '+message) def run(self): + if not escpos: + _logger.error('ESC/POS cannot initialize, please verify system dependencies.') + return while True: try: timestamp, task, data = self.queue.get(True) diff --git a/addons/im/security/ir.model.access.csv b/addons/im/security/ir.model.access.csv index 5137990498a..d35d694661a 100644 --- a/addons/im/security/ir.model.access.csv +++ b/addons/im/security/ir.model.access.csv @@ -1,4 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_im_message,im.message,model_im_message,,1,0,1,0 access_im_user,im.user,model_im_user,,1,1,1,0 -access_im_session,im.session,model_im_session,,1,0,0,0
\ No newline at end of file +access_im_session,im.session,model_im_session,,1,1,1,0
\ No newline at end of file diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index eea95a6c5ed..97abd6050e8 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -365,10 +365,11 @@ class mail_thread(osv.AbstractModel): track_ctx = dict(context) if 'lang' not in track_ctx: track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang - tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx) - if tracked_fields: - initial_values = {thread_id: dict((item, False) for item in tracked_fields)} - self.message_track(cr, uid, [thread_id], tracked_fields, initial_values, context=track_ctx) + if not context.get('mail_notrack'): + tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx) + if tracked_fields: + initial_values = {thread_id: dict((item, False) for item in tracked_fields)} + self.message_track(cr, uid, [thread_id], tracked_fields, initial_values, context=track_ctx) return thread_id def write(self, cr, uid, ids, values, context=None): @@ -392,7 +393,11 @@ class mail_thread(osv.AbstractModel): result = super(mail_thread, self).write(cr, uid, ids, values, context=context) self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context, values=values) - # Perform the tracking + if not context.get('mail_notrack'): + # Perform the tracking + tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context) + else: + tracked_fields = None if tracked_fields: self.message_track(cr, uid, ids, tracked_fields, initial_values, context=track_ctx) return result @@ -413,6 +418,9 @@ class mail_thread(osv.AbstractModel): return res def copy(self, cr, uid, id, default=None, context=None): + # avoid tracking multiple temporary changes during copy + context = dict(context or {}, mail_notrack=True) + default = default or {} default['message_ids'] = [] default['message_follower_ids'] = [] @@ -724,13 +732,15 @@ class mail_thread(osv.AbstractModel): # Private message: should not contain any thread_id if not model and thread_id: if assert_model: - assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id - _warn('posting a message without model should be with a null res_id (private message), received %s, resetting thread_id' % thread_id) + if thread_id: + raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id) + _warn('posting a message without model should be with a null res_id (private message), received %s resetting thread_id' % thread_id) thread_id = 0 # Private message: should have a parent_id (only answers) if not model and not message_dict.get('parent_id'): if assert_model: - assert message_dict.get('parent_id'), 'Routing: posting a message without model should be with a parent_id (private mesage).' + if not message_dict.get('parent_id'): + raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).') _warn('posting a message without model should be with a parent_id (private mesage), skipping') return () @@ -759,7 +769,10 @@ class mail_thread(osv.AbstractModel): # New Document: check model accepts the mailgateway if not thread_id and model and not hasattr(model_pool, 'message_new'): if assert_model: - assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model + if not hasattr(model_pool, 'message_new'): + raise ValueError( + 'Model %s does not accept document creation, crashing' % model + ) _warn('model %s does not accept document creation, skipping' % model) return () @@ -820,8 +833,11 @@ class mail_thread(osv.AbstractModel): to which this mail should be attached. Only used if the message does not reply to an existing thread and does not match any mail alias. :return: list of [model, thread_id, custom_values, user_id, alias] + + :raises: ValueError, TypeError """ - assert isinstance(message, Message), 'message must be an email.message.Message at this point' + if not isinstance(message, Message): + raise TypeError('message must be an email.message.Message at this point') mail_msg_obj = self.pool['mail.message'] fallback_model = model @@ -939,9 +955,11 @@ class mail_thread(osv.AbstractModel): return [route] # AssertionError if no routes found and if no bounce occured - assert False, \ - "No possible route found for incoming message from %s to %s (Message-Id %s:)." \ - "Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id) + raise ValueError( + 'No possible route found for incoming message from %s to %s (Message-Id %s:). ' + 'Create an appropriate mail.alias or force the destination model.' % + (email_from, email_to, message_id) + ) def message_route_process(self, cr, uid, message, message_dict, routes, context=None): # postpone setting message_dict.partner_ids after message_post, to avoid double notifications @@ -952,9 +970,11 @@ class mail_thread(osv.AbstractModel): context.update({'thread_model': model}) if model: model_pool = self.pool[model] - assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ - "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \ - (message_dict['message_id'], model) + if not (thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new')): + raise ValueError( + "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % + (message_dict['message_id'], model) + ) # disabled subscriptions during message_new/update to avoid having the system user running the # email gateway become a follower of all inbound messages @@ -964,7 +984,8 @@ class mail_thread(osv.AbstractModel): else: thread_id = model_pool.message_new(cr, user_id, message_dict, custom_values, context=nosub_ctx) else: - assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." + if thread_id: + raise ValueError("Posting a message without model should be with a null res_id, to create a private message.") model_pool = self.pool.get('mail.thread') if not hasattr(model_pool, 'message_post'): context['thread_model'] = model @@ -1548,6 +1569,9 @@ class mail_thread(osv.AbstractModel): """ Add partners to the records followers. """ if context is None: context = {} + # not necessary for computation, but saves an access right check + if not partner_ids: + return True mail_followers_obj = self.pool.get('mail.followers') subtype_obj = self.pool.get('mail.message.subtype') @@ -1556,6 +1580,9 @@ class mail_thread(osv.AbstractModel): if set(partner_ids) == set([user_pid]): try: self.check_access_rights(cr, uid, 'read') + if context.get('operation', '') == 'create': + self.check_access_rule(cr, uid, ids, 'create') + else: self.check_access_rule(cr, uid, ids, 'read') except (osv.except_osv, orm.except_orm): return False @@ -1603,6 +1630,9 @@ class mail_thread(osv.AbstractModel): def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None): """ Remove partners from the records followers. """ + # not necessary for computation, but saves an access right check + if not partner_ids: + return True user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] if set(partner_ids) == set([user_pid]): self.check_access_rights(cr, uid, 'read') diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index bfae7571a9c..50e190c846d 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -115,6 +115,9 @@ .openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p { margin-bottom: 0px; } +.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body pre { + white-space: pre-wrap; +} .openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body * { text-overflow:ellipsis; word-wrap: break-word; diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 58d44bab50e..b8bf703f604 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -925,20 +925,12 @@ openerp.mail = function (session) { }, on_record_clicked: function (event) { - event.stopPropagation(); var state = { 'model': this.model, 'id': this.res_id, 'title': this.record_name }; session.webclient.action_manager.do_push_state(state); - this.do_action({ - res_model: state.model, - res_id: state.id, - type: 'ir.actions.act_window', - views: [[false, 'form']] - }); - return false; }, /* Call the on_compose_message on the thread of this message. */ diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index 938d6a01f6d..c103e4d4ab2 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -197,6 +197,7 @@ openerp_mail_followers = function(session, mail) { // clean and display title var node_user_list = this.$('.oe_follower_list').empty(); this.$('.oe_follower_title').html(this._format_followers(this.followers.length)); + self.message_is_follower = _.indexOf(this.followers.map(function (rec) { return rec[2]['is_uid']}), true) != -1; // truncate number of displayed followers var truncated = this.followers.slice(0, this.displayed_nb); _(truncated).each(function (record) { @@ -207,9 +208,6 @@ openerp_mail_followers = function(session, mail) { 'is_editable': record[2]['is_editable'], 'avatar_url': mail.ChatterUtils.get_image(self.session, 'res.partner', 'image_small', record[0]), } - if (partner.is_uid) { - self.message_is_follower = partner.is_uid; - } $(session.web.qweb.render('mail.followers.partner', {'record': partner, 'widget': self})).appendTo(node_user_list); // On mouse-enter it will show the edit_subtype pencil. if (partner.is_editable) { diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 3a7a5469ee1..6ce588e82a2 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -135,14 +135,14 @@ <t t-if="widget.is_private or (widget.user_pid != partner[0])"> <t t-if="!widget.is_private and inc==0"> and </t> <span t-attf-class="oe_partner_follower #{inc>=3?'oe_hidden':''}"><t t-if="inc" t-raw="', '"/> - <a t-if="widget.options.show_link" t-attf-href="#model=res.partner&id=#{partner[0]}"><t t-raw="partner[1]"/></a> - <t t-if="!widget.options.show_link" t-raw="partner[1]"/> + <a t-if="widget.options.show_link" t-attf-href="#model=res.partner&id=#{partner[0]}"><t t-esc="partner[1]"/></a> + <t t-if="!widget.options.show_link" t-esc="partner[1]"/> </span> <t t-set="inc" t-value="inc+1"/> </t> </t> <t t-if="widget.partner_ids.length > 3"> - <span class="oe_more">, <a><t t-raw="widget.partner_ids.length - 3"/> others...</a></span> + <span class="oe_more">, <a><t t-esc="widget.partner_ids.length - 3"/> others...</a></span> <a class="oe_more_hidden"><<<</a> </t> </div> @@ -150,8 +150,8 @@ <t t-foreach='widget.recipients' t-as='recipient'> <label t-attf-title="Add as recipient and follower (reason: #{recipient.reason})"> <input type="checkbox" t-att-checked="recipient.checked ? 'checked' : undefined" t-att-data="recipient.full_name"/> - <t t-raw="recipient.name"/> - <t t-if="recipient.email_address">(<t t-raw="recipient.email_address"/>)</t> + <t t-esc="recipient.name"/> + <t t-if="recipient.email_address">(<t t-esc="recipient.email_address"/>)</t> <t t-if="!recipient.email_address">(no email address)</t> </label> </t> @@ -177,7 +177,7 @@ <td colspan="2"> <h2 class="oe_view_title"> <span class="oe_view_title_text"> - <t t-raw="widget.action.name"/> + <t t-esc="widget.action.name"/> </span> </h2> <t t-if="widget.action.params.header_description"> @@ -262,12 +262,13 @@ <!-- message itself --> <div class="oe_msg_content"> <h1 t-if="(widget.show_record_name or widget.subject) and !widget.thread_level" class="oe_msg_title"> - <a t-if="widget.options.show_link and widget.show_record_name" class="oe_mail_action_model" t-attf-href="#model=#{widget.model}&id=#{widget.res_id}"> - <t t-raw="widget.record_name"/> + <a t-if="widget.options.show_link and widget.show_record_name" class="oe_mail_action_model" + t-attf-href="#action=mail.action_mail_redirect&model=#{widget.model}&res_id=#{widget.res_id}"> + <t t-esc="widget.record_name"/> </a> - <span t-if="!widget.options.show_link and widget.show_record_name"><t t-raw="widget.record_name"/></span> + <span t-if="!widget.options.show_link and widget.show_record_name"><t t-esc="widget.record_name"/></span> <t t-if="widget.show_record_name and widget.subject">: </t> - <t t-if="widget.subject" t-raw="widget.subject"/> + <t t-if="widget.subject" t-esc="widget.subject"/> </h1> <div class="oe_msg_body"> <t t-if="widget.body_short"> @@ -283,8 +284,8 @@ <t t-if="widget.attachment_ids.length > 0"> <div class="oe_msg_attachment_list"></div> </t> - <a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&id=#{widget.author_id[0]}"><t t-raw="widget.author_id[2]"/></a> - <span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-raw="widget.author_id[2]"/></span> + <a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&id=#{widget.author_id[0]}"><t t-esc="widget.author_id[2]"/></a> + <span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-esc="widget.author_id[2]"/></span> <t t-if="widget.type == 'notification'"> updated document <t t-if="widget.partner_ids.length > 0"> @@ -303,20 +304,20 @@ <t t-if="widget.type == 'notification' or ( (widget.type == 'email' or widget.type == 'comment') and (widget.subtype or widget.partner_ids.length > 0))" t-foreach="widget.partner_ids.slice(0, 3)" t-as="partner"> <span t-attf-class="oe_partner_follower"> - <a t-if="widget.options.show_link" t-attf-href="#model=res.partner&id=#{partner[0]}"><t t-raw="partner[1]"/></a> - <t t-if="!widget.options.show_link" t-raw="partner[1]"/> + <a t-if="widget.options.show_link" t-attf-href="#model=res.partner&id=#{partner[0]}"><t t-esc="partner[1]"/></a> + <t t-if="!widget.options.show_link" t-esc="partner[1]"/> </span> <t t-if="!partner_last">,</t> </t> <t t-if="widget.partner_ids.length > 3"> - <span t-att-title="widget.extra_partners_str">and <t t-raw="widget.extra_partners_nbr"/> more</span> + <span t-att-title="widget.extra_partners_str">and <t t-esc="widget.extra_partners_nbr"/> more</span> </t> <t t-if="widget.type == 'notification' and widget.partner_ids.length > 0"> notified </t> <span class='oe_subtle'>•</span> <span t-att-title="widget.date"> - <t t-if="widget.timerelative" t-raw="widget.timerelative"/> + <t t-if="widget.timerelative" t-esc="widget.timerelative"/> <t t-if="!widget.timerelative" t-raw="widget.display_date"/> </span> <span t-if="!widget.options.readonly" class='oe_subtle'>•</span> @@ -333,7 +334,7 @@ <div class='oe_separator'></div> <a t-if="widget.nb_messages <= 0" class="oe_msg_fetch_more">show more message</a> <a t-if="widget.nb_messages === 1" class="oe_msg_fetch_more">show one more message</a> - <a t-if="widget.nb_messages > 1" class="oe_msg_fetch_more">show <t t-raw="widget.nb_messages" /> more messages</a> + <a t-if="widget.nb_messages > 1" class="oe_msg_fetch_more">show <t t-esc="widget.nb_messages" /> more messages</a> </div> </div> </t> @@ -353,7 +354,7 @@ --> <span t-name="mail.thread.message.vote"> <span class="oe_mail_vote_count" t-if='widget.vote_nb > 0'> - <t t-raw='widget.vote_nb' /> + <t t-esc='widget.vote_nb' /> <span class='oe_e'>8</span> </span> <a href='#' class="oe_msg_vote"> diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml index a936b8237d6..6e8682cb533 100644 --- a/addons/mail/static/src/xml/mail_followers.xml +++ b/addons/mail/static/src/xml/mail_followers.xml @@ -43,13 +43,13 @@ <table class='oe_subtype'> <tr> <td width="10%"><input type="checkbox" t-att-checked="record.followed" t-att-id="'input_mail_followers_subtype_'+record.id" t-att-data-id="record.id" t-att-name="record.name" class="oe_msg_subtype_check"/></td> - <td><label t-att-for="'input_mail_followers_subtype_'+record.id"><t t-raw="record.name"/></label></td> + <td><label t-att-for="'input_mail_followers_subtype_'+record.id"><t t-esc="record.name"/></label></td> </tr> </table> </t> <t t-name="mail.followers.show_more"> - <div class="oe_partner oe_show_more">And <t t-raw="number"/> more.</div> + <div class="oe_partner oe_show_more">And <t t-esc="number"/> more.</div> </t> </template> diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index c6fffbde9fe..cb735ec098e 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -394,7 +394,7 @@ class TestMailgateway(TestMail): 'message_process: after reply, group should have 2 followers') # Do: incoming email with ref holding model / res_id but that does not match any message in the thread: must raise since OpenERP saas-3 - self.assertRaises(AssertionError, + self.assertRaises(ValueError, format_and_process, MAIL_TEMPLATE, email_from='other5@gmail.com', to='noone@example.com', subject='spam', @@ -490,14 +490,14 @@ class TestMailgateway(TestMail): # -------------------------------------------------- # Do: incoming email with model that does not accepts incoming emails must raise - self.assertRaises(AssertionError, + self.assertRaises(ValueError, format_and_process, MAIL_TEMPLATE, to='noone@example.com', subject='spam', extra='', model='res.country', msg_id='<1198923581.41972151344608186760.JavaMail.new4@agrolait.com>') # Do: incoming email without model and without alias must raise - self.assertRaises(AssertionError, + self.assertRaises(ValueError, format_and_process, MAIL_TEMPLATE, to='noone@example.com', subject='spam', extra='', diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 74aea3dfc01..2d09a925ca7 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -130,7 +130,7 @@ class mail_compose_message(osv.TransientModel): 'wizard_id', 'attachment_id', 'Attachments'), 'filter_id': fields.many2one('ir.filters', 'Filters'), } - + #TODO change same_thread to False in trunk (Require view update) _defaults = { 'composition_mode': 'comment', 'body': lambda self, cr, uid, ctx={}: '', @@ -268,6 +268,8 @@ class mail_compose_message(osv.TransientModel): 'mail.message', 0, context=context) mail_values['attachment_ids'] = m2m_attachment_ids + if not mail_values.get('reply_to'): + mail_values['reply_to'] = mail_values['email_from'] self.pool.get('mail.mail').create(cr, uid, mail_values, context=context) else: subtype = 'mail.mt_comment' @@ -321,10 +323,10 @@ class mail_compose_message(osv.TransientModel): if email_dict.get('email_from'): mail_values['email_from'] = email_dict.pop('email_from') # replies redirection: mass mailing only - if not wizard.same_thread: - mail_values['reply_to'] = email_dict.pop('reply_to', None) - else: + if wizard.same_thread and wizard.post: email_dict.pop('reply_to', None) + else: + mail_values['reply_to'] = email_dict.pop('reply_to', None) mail_values.update(email_dict) # mass mailing without post: mail_mail values if mass_mail_mode and not wizard.post: diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 948d9b3832c..23931649402 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -47,10 +47,10 @@ <field name="notify" attrs="{'invisible':['|', ('post', '!=', True), ('composition_mode', '!=', 'mass_mail')]}"/> <field name="same_thread" - attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/> + attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_mail'), ('post', '=', False)]}"/> <field name="reply_to" placeholder="Email address te redirect replies..." - attrs="{'invisible':['|', ('same_thread', '=', True), ('composition_mode', '!=', 'mass_mail')], - 'required':[('same_thread', '!=', True)]}"/> + attrs="{'invisible':['|', '&', ('same_thread', '=', True), ('post', '=', True), ('composition_mode', '!=', 'mass_mail')], + 'required':['&', '|', ('post', '=', False), ('same_thread', '=', False), ('composition_mode', '=', 'mass_mail')]}"/> </group> <field name="body"/> <field name="attachment_ids" widget="many2many_binary" string="Attach a file"/> diff --git a/addons/mass_mailing/mail_thread.py b/addons/mass_mailing/mail_thread.py index 0570596ba12..a4ff15a1a3f 100644 --- a/addons/mass_mailing/mail_thread.py +++ b/addons/mass_mailing/mail_thread.py @@ -48,6 +48,7 @@ class MailThread(osv.Model): if bounce_alias in email_to: bounce_match = tools.bounce_re.search(email_to) if bounce_match: + bounced_model, bounced_thread_id = None, False bounced_mail_id = bounce_match.group(1) stat_ids = self.pool['mail.mail.statistics'].set_bounced(cr, uid, mail_mail_ids=[bounced_mail_id], context=context) for stat in self.pool['mail.mail.statistics'].browse(cr, uid, stat_ids, context=context): @@ -55,7 +56,7 @@ class MailThread(osv.Model): bounced_thread_id = stat.res_id _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s', email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id) - if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'): + if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce') and bounced_thread_id: self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context) return False diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py index 516e68db3d9..e7583d8e739 100644 --- a/addons/mass_mailing/mass_mailing.py +++ b/addons/mass_mailing/mass_mailing.py @@ -168,8 +168,10 @@ class MassMailing(osv.Model): 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'), } for i in range(0, self._period_number)] group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context) + field_col_info = obj._all_columns.get(groupby_field.split(':')[0]) + pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT for group in group_obj: - group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT).date() + group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date() timedelta = relativedelta.relativedelta(group_begin_date, date_begin) section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)} return section_result diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py index 5266147fef0..8b93e2a9bb6 100644 --- a/addons/mrp/procurement.py +++ b/addons/mrp/procurement.py @@ -111,9 +111,6 @@ class procurement_order(osv.osv): bom_result = production_obj.action_compute(cr, uid, [produce_id], properties=[x.id for x in procurement.property_ids]) production_obj.signal_button_confirm(cr, uid, [produce_id]) - if res_id: - move_obj.write(cr, uid, [res_id], - {'location_id': procurement.location_id.id}) self.production_order_create_note(cr, uid, ids, context=context) return res diff --git a/addons/pad/py_etherpad/__init__.py b/addons/pad/py_etherpad/__init__.py index fce51d36890..0db607fe16f 100644 --- a/addons/pad/py_etherpad/__init__.py +++ b/addons/pad/py_etherpad/__init__.py @@ -1,7 +1,7 @@ """Module to talk to EtherpadLite API.""" import json -import werkzeug.urls +import urllib import urllib2 @@ -32,7 +32,7 @@ class EtherpadLiteClient: params = arguments or {} params.update({'apikey': self.apiKey}) - data = werkzeug.url_encode(params, True) + data = urllib.urlencode(params, True) try: opener = urllib2.build_opener() diff --git a/addons/pad/static/src/css/etherpad.css b/addons/pad/static/src/css/etherpad.css index facf15b28de..d8281ef4d48 100644 --- a/addons/pad/static/src/css/etherpad.css +++ b/addons/pad/static/src/css/etherpad.css @@ -13,13 +13,12 @@ border: solid 1px #ccc; border-radius:3px; text-align: center; - line-height: 22px; + line-height: 28px; overflow: hidden; -webkit-box-sizing: border-box; color: #666666; - padding-top:-3px; - padding-left:-1px; cursor: pointer; + font-size: 14px; } .oe_pad_switch:hover{ diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index ba5ee3a3037..2d7f2c2737f 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -13,6 +13,8 @@ openerp.pad = function(instance) { this.switch_configured(); this.$('.oe_pad_switch').click(function() { self.$el.toggleClass('oe_pad_fullscreen'); + self.$el.find('.oe_pad_switch').toggleClass('fa-expand fa-compress'); + self.view.$el.find('.oe_chatter').toggle(); }); this.render_value(); }, diff --git a/addons/pad/static/src/xml/pad.xml b/addons/pad/static/src/xml/pad.xml index f915e867b35..5d2ab779ead 100644 --- a/addons/pad/static/src/xml/pad.xml +++ b/addons/pad/static/src/xml/pad.xml @@ -15,7 +15,7 @@ </t> <t t-if="! widget.get('effective_readonly')"> <div class="oe_pad_switch_positioner oe_configured"> - <span class="oe_pad_switch oe_e">&Ntilde;</span> + <span class="fa fa-expand oe_pad_switch"></span> </div> <div class="oe_pad_content oe_editing oe_configured"> </div> diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 0d97fdb1a0b..fbdf48aacf1 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -474,11 +474,12 @@ class pos_session(osv.osv): account_move_obj = self.pool.get('account.move') pos_order_obj = self.pool.get('pos.order') for session in self.browse(cr, uid, ids, context=context): + local_context = dict(context or {}, force_company=session.config_id.journal_id.company_id.id) order_ids = [order.id for order in session.order_ids if order.state == 'paid'] - move_id = account_move_obj.create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context) + move_id = account_move_obj.create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=local_context) - pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=context) + pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context) for order in session.order_ids: if order.state not in ('paid', 'invoiced'): @@ -948,22 +949,16 @@ class pos_order(osv.osv): # Tricky, via the workflow, we only have one id in the ids variable """Create a account move line of order grouped by products or not.""" account_move_obj = self.pool.get('account.move') - account_move_line_obj = self.pool.get('account.move.line') account_period_obj = self.pool.get('account.period') account_tax_obj = self.pool.get('account.tax') - user_proxy = self.pool.get('res.users') property_obj = self.pool.get('ir.property') cur_obj = self.pool.get('res.currency') - period = account_period_obj.find(cr, uid, context=context)[0] - #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!')) - current_company = user_proxy.browse(cr, uid, uid, context=context).company_id - grouped_data = {} have_to_group_by = session and session.config_id.group_by or False @@ -983,7 +978,7 @@ class pos_order(osv.osv): if order.state != 'paid': continue - user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id + current_company = order.sale_journal.company_id group_tax = {} account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context) @@ -1004,6 +999,7 @@ class pos_order(osv.osv): # if have_to_group_by: sale_journal_id = order.sale_journal.id + period = account_period_obj.find(cr, uid, context=dict(context or {}, company_id=current_company.id))[0] # 'quantity': line.qty, # 'product_id': line.product_id.id, @@ -1013,7 +1009,7 @@ class pos_order(osv.osv): 'journal_id' : sale_journal_id, 'period_id' : period, 'move_id' : move_id, - 'company_id': user_company and user_company.id or False, + 'company_id': current_company.id, }) if data_type == 'product': @@ -1054,7 +1050,10 @@ class pos_order(osv.osv): cur = order.pricelist_id.currency_id for line in order.lines: tax_amount = 0 - taxes = [t for t in line.product_id.taxes_id] + taxes = [] + for t in line.product_id.taxes_id: + if t.company_id.id == current_company.id: + taxes.append(t) computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes'] for tax in computed_taxes: diff --git a/addons/point_of_sale/report/pos_details_summary.py b/addons/point_of_sale/report/pos_details_summary.py index a4facaabcac..d6600926bd6 100644 --- a/addons/point_of_sale/report/pos_details_summary.py +++ b/addons/point_of_sale/report/pos_details_summary.py @@ -99,13 +99,10 @@ class pos_details_summary(report_sxw.rml_parse): def _get_tax_amount(self, objects): res = {} - list_ids = [] for order in objects: for line in order.lines: - if len(line.product_id.taxes_id): - tax = line.product_id.taxes_id[0] - res[tax.name] = (line.price_unit * line.qty * (1-(line.discount or 0.0) / 100.0)) + (tax.id in list_ids and res[tax.name] or 0) - list_ids.append(tax.id) + for tax in line.product_id.taxes_id: + res[tax.name] = res.setdefault(tax.name, 0.0) + (line.price_subtotal_incl - line.price_subtotal) return res def _get_sales_total(self, objects): diff --git a/addons/point_of_sale/report/pos_details_summary.rml b/addons/point_of_sale/report/pos_details_summary.rml index b1c71db7d40..6564f821010 100644 --- a/addons/point_of_sale/report/pos_details_summary.rml +++ b/addons/point_of_sale/report/pos_details_summary.rml @@ -232,7 +232,7 @@ <blockTable colWidths="255.0,255.0" style="Table11"> <tr> <td> - <para style="P2">[[ t[0] ]]</para> + <para style="P2">[[ t[0].name ]]</para> </td> <td> <para style="terp_default_Right_9_Bold">[[ formatLang(t[1], currency_obj=company.currency_id) ]]</para> diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py index 279f921f6c9..65c4311f34a 100644 --- a/addons/product/pricelist.py +++ b/addons/product/pricelist.py @@ -244,13 +244,15 @@ class product_pricelist(osv.osv): for seller in product.seller_ids: if (not partner) or (seller.name.id<>partner): continue - product_default_uom = product.uom_id.id + qty_in_seller_uom = qty + from_uom = context.get('uom') or product.uom_id.id seller_uom = seller.product_uom and seller.product_uom.id or False - if seller_uom and product_default_uom and product_default_uom != seller_uom: + if seller_uom and from_uom and from_uom != seller_uom: + qty_in_seller_uom = product_uom_obj._compute_qty(cr, uid, from_uom, qty, to_uom_id=seller_uom) + else: uom_price_already_computed = True - qty = product_uom_obj._compute_qty(cr, uid, product_default_uom, qty, to_uom_id=seller_uom) for line in seller.pricelist_ids: - if line.min_quantity <= qty: + if line.min_quantity <= qty_in_seller_uom: price = line.price else: @@ -277,7 +279,6 @@ class product_pricelist(osv.osv): if price: if 'uom' in context and not uom_price_already_computed: - product = products_dict[product.id] uom = product.uos_id or product.uom_id price = product_uom_obj._compute_price(cr, uid, uom.id, price, context['uom']) diff --git a/addons/project/project.py b/addons/project/project.py index f03a4242de3..e5dcf5de2cf 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -358,6 +358,11 @@ class project(osv.osv): default['state'] = 'open' default['line_ids'] = [] default['tasks'] = [] + + # Don't prepare (expensive) data to copy children (analytic accounts), + # they are discarded in analytic.copy(), and handled in duplicate_template() + default['child_ids'] = [] + proj = self.browse(cr, uid, id, context=context) if not default.get('name', False): default.update(name=_("%s (copy)") % (proj.name)) @@ -690,23 +695,13 @@ class task(osv.osv): return {'value': vals} def duplicate_task(self, cr, uid, map_ids, context=None): - for new in map_ids.values(): - task = self.browse(cr, uid, new, context) - child_ids = [ ch.id for ch in task.child_ids] - if task.child_ids: - for child in task.child_ids: - if child.id in map_ids.keys(): - child_ids.remove(child.id) - child_ids.append(map_ids[child.id]) - - parent_ids = [ ch.id for ch in task.parent_ids] - if task.parent_ids: - for parent in task.parent_ids: - if parent.id in map_ids.keys(): - parent_ids.remove(parent.id) - parent_ids.append(map_ids[parent.id]) - #FIXME why there is already the copy and the old one - self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]}) + mapper = lambda t: map_ids.get(t.id, t.id) + for task in self.browse(cr, uid, map_ids.values(), context): + new_child_ids = set(map(mapper, task.child_ids)) + new_parent_ids = set(map(mapper, task.parent_ids)) + if new_child_ids or new_parent_ids: + task.write({'parent_ids': [(6,0,list(new_parent_ids))], + 'child_ids': [(6,0,list(new_child_ids))]}) def copy_data(self, cr, uid, id, default=None, context=None): if default is None: diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index e40d219ef10..4fa0e760df1 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -176,7 +176,7 @@ <button name="invoice_ok" states="except_invoice" string="Manually Corrected"/> <button name="purchase_approve" states="confirmed" string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/> <button name="view_picking" string="Receive Products" type="object" attrs="{'invisible': ['|', ('shipped','=',True), ('state','!=', 'approved')]}" class="oe_highlight"/> - <button name="view_invoice" string="Receive Invoice" type="object" attrs="{'invisible': ['|', ('invoice_method','=','picking'), '|', ('state','!=', 'approved'), ('invoiced','=',True) ]}" class="oe_highlight"/> + <button name="view_invoice" string="Receive Invoice" type="object" attrs="{'invisible': ['|', ('invoice_method','in', ['picking', 'manual']), '|', ('state','!=', 'approved'), ('invoiced','=',True) ]}" class="oe_highlight"/> <button name="action_cancel_draft" states="cancel,confirmed" string="Set to Draft" type="object" /> <button name="purchase_cancel" states="draft,confirmed,sent" string="Cancel"/> <field name="state" widget="statusbar" statusbar_visible="draft,sent,approved,done" statusbar_colors='{"except_picking":"red","except_invoice":"red","confirmed":"blue"}' readonly="1"/> diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 8848ba9df2b..a8851c654f8 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -1014,8 +1014,8 @@ class account_invoice(osv.Model): sale_order_obj = self.pool.get('sale.order') res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context) so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context) - if so_ids: - sale_order_obj.message_post(cr, uid, so_ids, body=_("Invoice paid"), context=context) + for so_id in so_ids: + sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context) return res def unlink(self, cr, uid, ids, context=None): diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 04a9972b9b0..35901b0131e 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -77,9 +77,9 @@ <button name="invoice_recreate" states="invoice_except" string="Recreate Invoice" groups="base.group_user"/> <button name="invoice_corrected" states="invoice_except" string="Ignore Exception" groups="base.group_user"/> <button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="oe_highlight" groups="base.group_user"/> - <button name="action_quotation_send" string="Send by Email" type="object" states="sent" groups="base.group_user"/> + <button name="action_quotation_send" string="Send by Email" type="object" states="sent,progress,manual" groups="base.group_user"/> <button name="print_quotation" string="Print" type="object" states="draft" class="oe_highlight" groups="base.group_user"/> - <button name="print_quotation" string="Print" type="object" states="sent" groups="base.group_user"/> + <button name="print_quotation" string="Print" type="object" states="sent,progress,manual" groups="base.group_user"/> <button name="action_button_confirm" states="draft" string="Confirm Sale" type="object" groups="base.group_user"/> <button name="action_button_confirm" states="sent" string="Confirm Sale" class="oe_highlight" type="object" groups="base.group_user"/> <button name="action_view_invoice" string="View Invoice" type="object" class="oe_highlight" @@ -90,7 +90,7 @@ <button name="cancel" states="draft,sent" string="Cancel Quotation" groups="base.group_user"/> <button name="action_cancel" states="manual,progress" string="Cancel Order" type="object" groups="base.group_user"/> <button name="invoice_cancel" states="invoice_except" string="Cancel Order" groups="base.group_user"/> - <field name="state" widget="statusbar" statusbar_visible="draft,sent,invoiced,done" statusbar_colors='{"invoice_except":"red","waiting_date":"blue"}'/> + <field name="state" widget="statusbar" statusbar_visible="draft,sent,progress,done" statusbar_colors='{"invoice_except":"red","waiting_date":"blue"}'/> </header> <sheet> <h1> @@ -231,7 +231,7 @@ <field name="name">sale.order.form.editable.list</field> <field name="model">sale.order</field> <field name="inherit_id" ref="sale.view_order_form"/> - <field name="groups_id" eval="[(6, 0, [ref('product.group_uos'), ref('product.group_stock_packaging'), ref('sale.group_mrp_properties')])]"/> + <field name="groups_id" eval="[(4, ref('product.group_uos')), (4, ref('product.group_stock_packaging')), (4, ref('sale.group_mrp_properties'))]"/> <field name="arch" type="xml"> <xpath expr="//field[@name='order_line']/tree" position="attributes"> <attribute name="editable"/> diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml index 7ed31586c17..c1e6498b429 100644 --- a/addons/sale_crm/sale_crm_view.xml +++ b/addons/sale_crm/sale_crm_view.xml @@ -214,8 +214,7 @@ <record id="action_order_report_quotation_salesteam" model="ir.actions.act_window"> <field name="name">Quotations Analysis</field> <field name="res_model">sale.report</field> - <field name="view_type">form</field> - <field name="view_mode">tree,graph</field> + <field name="view_mode">graph</field> <field name="domain">[('state','=','draft'),('section_id', '=', active_id)]</field> <field name="context">{'search_default_order_month':1}</field> <field name="help">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.</field> @@ -224,8 +223,7 @@ <record id="action_order_report_so_salesteam" model="ir.actions.act_window"> <field name="name">Sales Analysis</field> <field name="res_model">sale.report</field> - <field name="view_type">form</field> - <field name="view_mode">tree,graph</field> + <field name="view_mode">graph</field> <field name="domain">[('state','not in',('draft','sent','cancel')),('section_id', '=', active_id)]</field> <field name="context">{'search_default_order_month':1}</field> <field name="help">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.</field> @@ -234,8 +232,7 @@ <record id="action_account_invoice_report_salesteam" model="ir.actions.act_window"> <field name="name">Invoices Analysis</field> <field name="res_model">account.invoice.report</field> - <field name="view_type">form</field> - <field name="view_mode">tree,graph</field> + <field name="view_mode">graph</field> <field name="domain">[('section_id', '=', active_id),('state', 'not in', ['draft', 'cancel'])]</field> <field name="context">{'search_default_month':1}</field> <field name="help">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.</field> diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml index 81f16c5eb40..6d0214fba67 100644 --- a/addons/sale_stock/sale_stock_view.xml +++ b/addons/sale_stock/sale_stock_view.xml @@ -20,8 +20,8 @@ <xpath expr="//button[@name='action_cancel']" position="after"> <button name="ship_cancel" states="shipping_except" string="Cancel Order"/> </xpath> - <field name="state" position="replace"> - <field name="state" widget="statusbar" statusbar_visible="draft,sent,progress,invoiced,done" statusbar_colors='{"shipping_except":"red","invoice_except":"red","waiting_date":"blue"}'/> + <field name="state" position="attributes"> + <attribute name="statusbar_colors">{"shipping_except":"red","invoice_except":"red","waiting_date":"blue"}</attribute> </field> <field name="company_id" position="replace"> <field name="company_id" readonly="True"/> diff --git a/addons/stock/product.py b/addons/stock/product.py index 8161dc3ebaa..3eddf0a1c72 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -379,8 +379,6 @@ class product_product(osv.osv): "In a context with a single Warehouse, this includes " "goods stored in the Stock Location of this Warehouse, or any " "of its children.\n" - "stored in the Stock Location of the Warehouse of this Shop, " - "or any of its children.\n" "Otherwise, this includes goods stored in any Stock Location " "with 'internal' type."), 'incoming_qty': fields.function(_product_available, multi='qty_available', @@ -392,9 +390,6 @@ class product_product(osv.osv): "In a context with a single Warehouse, this includes " "goods arriving to the Stock Location of this Warehouse, or " "any of its children.\n" - "In a context with a single Shop, this includes goods " - "arriving to the Stock Location of the Warehouse of this " - "Shop, or any of its children.\n" "Otherwise, this includes goods arriving to any Stock " "Location with 'internal' type."), 'outgoing_qty': fields.function(_product_available, multi='qty_available', @@ -406,9 +401,6 @@ class product_product(osv.osv): "In a context with a single Warehouse, this includes " "goods leaving the Stock Location of this Warehouse, or " "any of its children.\n" - "In a context with a single Shop, this includes goods " - "leaving the Stock Location of the Warehouse of this " - "Shop, or any of its children.\n" "Otherwise, this includes goods leaving any Stock " "Location with 'internal' type."), 'track_production': fields.boolean('Track Manufacturing Lots', help="Forces to specify a Serial Number for all moves containing this product and generated by a Manufacturing Order"), diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 7bc701edde1..a39a1a73e7a 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -95,8 +95,15 @@ class Website(openerp.addons.web.controllers.main.Home): # Edit #------------------------------------------------------ @http.route('/website/add/<path:path>', type='http', auth="user", website=True) - def pagenew(self, path, noredirect=False): + def pagenew(self, path, noredirect=False, add_menu=None): xml_id = request.registry['website'].new_page(request.cr, request.uid, path, context=request.context) + if add_menu: + model, id = request.registry["ir.model.data"].get_object_reference(request.cr, request.uid, 'website', 'main_menu') + request.registry['website.menu'].create(request.cr, request.uid, { + 'name': path, + 'url': "/page/" + xml_id, + 'parent_id': id, + }, context=request.context) url = "/page/" + xml_id if noredirect: return werkzeug.wrappers.Response(url, mimetype='text/plain') diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index a789657dad3..27af03310a9 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -148,7 +148,7 @@ class view(osv.osv): qcontext.update(values) # in edit mode ir.ui.view will tag nodes - context['inherit_branding'] = qcontext['editable'] + context['inherit_branding'] = qcontext.get('editable', False) view_obj = request.website.get_template(id_or_xml_id) if 'main_object' not in qcontext: diff --git a/addons/website/models/website.py b/addons/website/models/website.py index c73de9d10a7..796e50721dc 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -178,10 +178,8 @@ class website(osv.osv): return '%s.%s' % (module, slugify(name, max_length=50)) def page_exists(self, cr, uid, ids, name, module='website', context=None): - page = self.page_for_name(cr, uid, ids, name, module=module, context=context) - try: - self.pool["ir.model.data"].get_object_reference(cr, uid, module, name) + return self.pool["ir.model.data"].get_object_reference(cr, uid, module, name) except: return False @@ -251,7 +249,7 @@ class website(osv.osv): pmin = pmax - scope if pmax - scope > 0 else 1 def get_url(page): - _url = "%spage/%s/" % (url, page) + _url = "%spage/%s/" % (url, page) if page > 1 else url if url_args: _url = "%s?%s" % (_url, werkzeug.url_encode(url_args)) return _url @@ -351,6 +349,7 @@ class website(osv.osv): router = request.httprequest.app.get_db_router(request.db) # Force enumeration to be performed as public user uid = self.get_public_user(cr, uid, context=context) + url_list = [] for rule in router.iter_rules(): if not self.rule_is_enumerable(rule): continue @@ -372,7 +371,9 @@ class website(osv.osv): for values in generated: domain_part, url = rule.build(values, append_unknown=False) page = {'name': url, 'url': url} - + if url in url_list: + continue + url_list.append(url) if not filtered and query_string and not self.page_matches(cr, uid, page, query_string, context=context): continue yield page @@ -497,9 +498,14 @@ class website_menu(osv.osv): 'parent_left': fields.integer('Parent Left', select=True), 'parent_right': fields.integer('Parent Right', select=True), } + + def __defaults_sequence(self, cr, uid, context): + menu = self.search_read(cr, uid, [(1,"=",1)], ["sequence"], limit=1, order="sequence DESC", context=context) + return menu and menu[0]["sequence"] or 0 + _defaults = { 'url': '', - 'sequence': 0, + 'sequence': __defaults_sequence, 'new_window': False, } _parent_store = True diff --git a/addons/website/static/src/css/snippets.css b/addons/website/static/src/css/snippets.css index 16e85850b2a..fe9c18c0081 100644 --- a/addons/website/static/src/css/snippets.css +++ b/addons/website/static/src/css/snippets.css @@ -270,6 +270,10 @@ -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); } +.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before { + content: "\f0d9-\f0da"; + line-height: 16px; +} .oe_overlay .oe_handle.e { left: auto; top: 2px; @@ -277,8 +281,17 @@ right: -6px; cursor: w-resize; } -.oe_overlay .oe_handle.e:before { - content: "\F061"; +.oe_overlay .oe_handle.w { + top: 2px; + height: 100%; + left: -6px; + cursor: e-resize; +} +.oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before { + z-index: 0; + content: "\f07d"; + text-align: center; + padding: 1px; } .oe_overlay .oe_handle.s { top: auto; @@ -287,9 +300,11 @@ bottom: -6px; cursor: n-resize; } -.oe_overlay .oe_handle.s:before { - z-index: 0; - content: "\F063"; +.oe_overlay .oe_handle.n { + left: 2px; + width: 100%; + top: -6px; + cursor: s-resize; } .oe_overlay .oe_handle.size { top: auto; @@ -320,24 +335,6 @@ position: relative; top: 8px; } -.oe_overlay .oe_handle.w { - top: 2px; - height: 100%; - left: -6px; - cursor: e-resize; -} -.oe_overlay .oe_handle.w:before { - content: "\F060"; -} -.oe_overlay .oe_handle.n { - left: 2px; - width: 100%; - top: -6px; - cursor: s-resize; -} -.oe_overlay .oe_handle.n:before { - content: "\F062"; -} .oe_overlay .icon.btn { display: inline-block; } @@ -362,6 +359,36 @@ .oe_overlay .oe_overlay_options .dropdown-menu select, .oe_overlay .oe_overlay_options .dropdown-menu input { display: block; } +.oe_overlay.block-w-left .w:before { + content: "\F061" !important; +} +.oe_overlay.block-w-right .w:before { + content: "\F060" !important; +} +.oe_overlay.block-w-left.block-w-right .w { + display: none !important; +} +.oe_overlay.block-e-left .e:before { + content: "\F061" !important; +} +.oe_overlay.block-e-right .e:before { + content: "\F060" !important; +} +.oe_overlay.block-e-left.block-e-right .e { + display: none !important; +} +.oe_overlay.block-s-top .s:before { + content: "\F063" !important; +} +.oe_overlay.block-s-bottom .s:before { + content: "\f062" !important; +} +.oe_overlay.block-n-top .n:before { + content: "\F063" !important; +} +.oe_overlay.block-n-bottom .n:before { + content: "\f062" !important; +} .s-resize-important, .s-resize-important * { cursor: s-resize !important; diff --git a/addons/website/static/src/css/snippets.sass b/addons/website/static/src/css/snippets.sass index 7e889763572..a06bba01dba 100644 --- a/addons/website/static/src/css/snippets.sass +++ b/addons/website/static/src/css/snippets.sass @@ -147,6 +147,7 @@ .resize_editor_busy background-color: rgba(0,0,0,0.3) + .oe_overlay display: none position: absolute @@ -194,23 +195,38 @@ background: rgba(0, 0, 0, .5) color: #fff +box-shadow(0 0 5px 3px rgba(255,255,255,.7)) + &.e, &.w + &:before + content: "\f0d9-\f0da" + line-height: 16px &.e left: auto top: 2px height: 100% right: -6px cursor: w-resize + &.w + top: 2px + height: 100% + left: -6px + cursor: e-resize + &.s, &.n &:before - content: "\F061" + z-index: 0 + content: "\f07d" + text-align: center + padding: 1px &.s top: auto left: 2px width: 100% bottom: -6px cursor: n-resize - &:before - z-index: 0 - content: "\F063" + &.n + left: 2px + width: 100% + top: -6px + cursor: s-resize &.size top: auto left: 2px @@ -234,20 +250,6 @@ border-color: rgba(0, 0, 0, 0.5) position: relative top: 8px - &.w - top: 2px - height: 100% - left: -6px - cursor: e-resize - &:before - content: "\F060" - &.n - left: 2px - width: 100% - top: -6px - cursor: s-resize - &:before - content: "\F062" .icon.btn display: inline-block @@ -269,6 +271,29 @@ .dropdown-menu select,.dropdown-menu input display: block + &.block-w-left .w:before + content: "\F061" !important + &.block-w-right .w:before + content: "\F060" !important + &.block-w-left.block-w-right .w + display: none !important + &.block-e-left .e:before + content: "\F061" !important + &.block-e-right .e:before + content: "\F060" !important + &.block-e-left.block-e-right .e + display: none !important + + &.block-s-top .s:before + content: "\F063" !important + &.block-s-bottom .s:before + content: "\f062" !important + &.block-n-top .n:before + content: "\F063" !important + &.block-n-bottom .n:before + content: "\f062" !important + + .s-resize-important, .s-resize-important * cursor: s-resize !important .n-resize-important, .n-resize-important * diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 317d9193d94..e47b7a41baa 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -298,20 +298,23 @@ ul.nav-stacked > li > a { } .carousel .carousel-caption { - left: auto; - right: auto; - bottom: 10%; + position: absolute; + right: 50%; + left: 50%; + bottom: 20px; +} +.carousel .carousel-caption > div { + position: absolute; text-align: left; padding: 20px; background: rgba(0, 0, 0, 0.4); + bottom: 20px; } .carousel .carousel-caption h1, .carousel .carousel-caption h2, .carousel .carousel-caption h3 { margin-top: 10px; margin-bottom: 10px; } .carousel .carousel-image { - right: 50%; - left: 50%; top: 5%; bottom: 5%; position: absolute; @@ -326,13 +329,35 @@ ul.nav-stacked > li > a { right: 10%; top: 10%; bottom: auto; +} +.carousel .item.text_only .carousel-caption > div { text-align: center; background: transparent; + bottom: auto; + width: 100%; +} +.carousel .item.text_image .carousel-caption { + left: 10%; +} +.carousel .item.text_image .carousel-caption > div { + right: 50%; + margin-right: -20%; + max-width: 550px; +} +.carousel .item.text_image .carousel-image { + right: 10%; + left: 50%; } .carousel .item.image_text .carousel-caption { right: 10%; } +.carousel .item.image_text .carousel-caption > div { + left: 50%; + margin-left: -20%; + max-width: 550px; +} .carousel .item.image_text .carousel-image { + right: 50%; left: 10%; } .carousel .carousel-control { diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index 2390464bfa7..3cdfe37a896 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -244,18 +244,20 @@ ul.nav-stacked > li > a background-size: cover .carousel .carousel-caption - left: auto - right: auto - bottom: 10% - text-align: left - padding: 20px - background: rgba(0, 0, 0, 0.4) + position: absolute + right: 50% + left: 50% + bottom: 20px + > div + position: absolute + text-align: left + padding: 20px + background: rgba(0, 0, 0, 0.4) + bottom: 20px h1, h2, h3 margin-top: 10px margin-bottom: 10px .carousel-image - right: 50% - left: 50% top: 5% bottom: 5% position: absolute @@ -269,12 +271,30 @@ ul.nav-stacked > li > a right: 10% top: 10% bottom: auto - text-align: center - background: transparent + > div + text-align: center + background: transparent + bottom: auto + width: 100% + .item.text_image + .carousel-caption + left: 10% + > div + right: 50% + margin-right: -20% + max-width: 550px + .carousel-image + right: 10% + left: 50% .item.image_text .carousel-caption right: 10% + > div + left: 50% + margin-left: -20% + max-width: 550px .carousel-image + right: 50% left: 10% .carousel-control cursor: pointer diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js index 514969fc4c2..ed7037e903c 100644 --- a/addons/website/static/src/js/website.ace.js +++ b/addons/website/static/src/js/website.ace.js @@ -41,12 +41,14 @@ isWellFormed: function () { if (document.implementation.createDocument) { var dom = new DOMParser().parseFromString(this.xml, "text/xml"); - return dom.getElementsByTagName("parsererror").length === 0; + var error = dom.getElementsByTagName("parsererror"); + return error.length === 0 || error; } else if (window.ActiveXObject) { // TODO test in IE var msDom = new ActiveXObject("Microsoft.XMLDOM"); msDom.async = false; - return !msDom.loadXML(this.xml); + msDom.loadXML(this.xml); + return !msDom.parseError.errorCode || msDom.parseError.reason + "\nline " + msDom.parseError.line; } return true; }, @@ -256,28 +258,35 @@ }), function (session) { return session.isDirty; }); - var requests = _.map(toSave, self.saveView); + this.clearError(); + var requests = _.map(toSave, function (session) { + return self.saveView(session); + }); $.when.apply($, requests).then(function () { self.reloadPage.call(self); - }).fail(function (source, error) { - var message = _.isString(error) ? error - : (error && error.data && error.data.arguments && error.data.arguments[0] === "Access Denied") ? "Access denied: please sign in" - : (error && error.message) ? error.message - : "Unexpected error"; - self.displayError.call(self, message); + }).fail(function (source, session, error) { + self.displayError.call(self, source, session, error); }); }, saveView: function (session) { + var self = this; var xml = new website.ace.XmlDocument(session.text); - if (xml.isWellFormed()) { - return openerp.jsonRpc('/web/dataset/call', 'call', { + var isWellFormed = xml.isWellFormed(); + var def = $.Deferred(); + if (isWellFormed === true) { + openerp.jsonRpc('/web/dataset/call', 'call', { model: 'ir.ui.view', method: 'write', args: [[session.id], { 'arch': xml.xml }, website.get_context()], + }).then(function () { + def.resolve(); + }).fail(function (source, error) { + def.reject("server", session, error); }); } else { - return $.Deferred().reject(null, "Malformed XML document"); + def.reject(null, session, $(isWellFormed).text()); } + return def; }, updateHash: function () { window.location.hash = hash + "?view=" + this.selectedViewId(); @@ -286,9 +295,61 @@ this.updateHash(); window.location.reload(); }, - displayError: function (error) { - // TODO Improve feedback (e.g. update 'Save' button + tooltip) - alert(error); + clearError: function () { + this.$(".ace_layer.ace_text-layer .ace_line").css("background", ""); + }, + displayError: function (source, session, error) { + var self = this; + var line, test; + // format error message + var message = _.isString(error) ? error + : (error && error.data && error.data.arguments && error.data.arguments[0] === "Access Denied") ? "Access denied: please sign in" + : (error && error.data && error.data.message) ? error.data.message + : (error && error.message) ? error.message + : "Unexpected error"; + if (source == "server") { + message = eval(message.replace(/^\(/g, '([') + .replace(/\)$/g, '])') + .replace(/u'/g, "'") + .replace(/<([^>]+)>/g, '<b style="color:#661100;"><\$1></b>'))[1]; + line = -1; + } else { + line = message.match(/line ([0-9]+)/i); + line = line ? parseInt(line[1],10) : -1; + test = new RegExp("^\\s*"+line+"\\s*$"); + } + + function gotoline() { + self.aceEditor.gotoLine(line); + setTimeout(function () { + var $lines = self.$(".ace_editor .ace_gutter .ace_gutter-cell"); + var index = $lines.filter(function () { + return test.test($(this).text()); + }).index(); + if (index>0) { + self.$(".ace_layer.ace_text-layer .ace_line:eq(" + index + ")").css("background", "#661100"); + } + },100); + } + function onchangeSession (e) { + self.aceEditor.off('changeSession', onchangeSession); + gotoline(); + } + + var $list = this.$("#ace-view-list"); + if (+$list.val() == session.id) { + if (line>-1) gotoline(); + } else { + if (line) self.aceEditor.on('changeSession', onchangeSession); + this.$("#ace-view-list").val(session.id).change(); + } + + var $dialog = $(openerp.qweb.render('website.error_dialog', { + title: session.text.match(/\s+name=['"]([^'"]+)['"]/i)[1], + message:"<b>Malformed XML document</b>:<br/>" + message + })); + $dialog.appendTo("body"); + $dialog.modal('show'); }, open: function () { this.$el.removeClass('oe_ace_closed').addClass('oe_ace_open'); diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 7e867dca685..dc82c0c17b9 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -994,6 +994,7 @@ start: function () { var sup = this._super(); this.$el.modal({backdrop: 'static'}); + this.$('input:first').focus(); return sup; }, save: function () { @@ -1033,10 +1034,13 @@ }, start: function () { var self = this; + var last; this.$('#link-page').select2({ minimumInputLength: 1, placeholder: _t("New or existing page"), query: function (q) { + if (q.term == last) return; + last = q.term; $.when( self.page_exists(q.term), self.fetch_pages(q.term) @@ -1667,6 +1671,8 @@ } switch(m.type) { case 'attributes': // ignore .cke_focus being added or removed + // ignore id modification + if (m.attributeName === 'id') { return false; } // if attribute is not a class, can't be .cke_focus change if (m.attributeName !== 'class') { return true; } diff --git a/addons/website/static/src/js/website.editor.newpage.js b/addons/website/static/src/js/website.editor.newpage.js index 8d3b3e9b6b5..b475223ecd9 100644 --- a/addons/website/static/src/js/website.editor.newpage.js +++ b/addons/website/static/src/js/website.editor.newpage.js @@ -13,16 +13,44 @@ this.$(".oe_content_menu li.divider").removeClass("hidden"); return res; }, + edit: function () { + this.on('rte:ready', this, function () { + $('a:has(span[data-oe-model="website.menu"])').tooltip({ + title: _t('Save this page and use the top "Content" menu to edit the menu.'), + placement: "bottom", + trigger: "hover", + show: 50, + hide: 100, + container: 'body' + }); + }); + return this._super(); + }, events: _.extend({}, website.EditorBar.prototype.events, { 'click a[data-action=new_page]': function (ev) { ev.preventDefault(); website.prompt({ id: "editor_new_page", window_title: _t("New Page"), - input: "Page Title", - }).then(function (val) { + input: _t("Page Title"), + init: function () { + var $group = this.$dialog.find("div.form-group"); + $group.removeClass("mb0"); + + var $add = $( + '<div class="form-group mb0">'+ + '<label class="col-sm-offset-3 col-sm-9 text-left">'+ + ' <input type="checkbox" checked="checked" required="required"/> '+ + '</label>'+ + '</div>'); + $add.find('label').append(_t("Add page in menu")); + $group.after($add); + } + }).then(function (val, field, $dialog) { if (val) { - document.location = '/website/add/' + encodeURI(val); + var url = '/website/add/' + encodeURI(val); + if ($dialog.find('input[type="checkbox"]').is(':checked')) url +="?add_menu=1"; + document.location = url; } }); } diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 6fde9495d36..671dfc14ee9 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -206,6 +206,7 @@ var def = $.Deferred(); var dialog = $(openerp.qweb.render('website.prompt', options)).appendTo("body"); + options.$dialog = dialog; var field = dialog.find(options.field_type).first(); field.val(options.default); field.fillWith = function (data) { @@ -226,7 +227,7 @@ dialog.modal('show'); field.focus(); dialog.on('click', '.btn-primary', function () { - def.resolve(field.val(), field); + def.resolve(field.val(), field, dialog); dialog.remove(); }); }); @@ -247,13 +248,13 @@ website.form = function (url, method, params) { var form = document.createElement('form'); - form.action = url; - form.method = method; + form.setAttribute('action', url); + form.setAttribute('method', method); _.each(params, function (v, k) { var param = document.createElement('input'); - param.type = 'hidden'; - param.name = k; - param.value = v; + param.setAttribute('type', 'hidden'); + param.setAttribute('name', k); + param.setAttribute('value', v); form.appendChild(param); }); document.body.appendChild(form); diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index 048cf20231f..bc2ea064731 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -2,7 +2,7 @@ 'use strict'; var website = openerp.website; - website.snippet = {}; + if (!website.snippet) website.snippet = {}; website.snippet.readyAnimation = []; website.snippet.start_animation = function () { diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index e45bee9685b..23c5956ebf4 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -79,6 +79,7 @@ // 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped // 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated + if (!website.snippet) website.snippet = {}; website.snippet.styles = {}; website.snippet.selector = []; website.snippet.BuildingBlock = openerp.Widget.extend({ @@ -139,9 +140,6 @@ this.$el.addClass("hidden"); - this.$modal = $(openerp.qweb.render('website.snippets_modal')); - this.$modal.appendTo("body"); - $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) { e.preventDefault(); }); @@ -375,7 +373,7 @@ }); }, stop: function(ev, ui){ - if (action === 'insert' && ! dropped && $('.oe_drop_zone') && ui.position.top > 50) { + if (action === 'insert' && ! dropped && $('.oe_drop_zone') && ui.position.top > 3) { var el = $('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first(); if (el.length) { el.after($toInsert); @@ -392,7 +390,7 @@ website.snippet.start_animation(); self.create_overlay($target); - if ($snippet.data("snippet-editor")) { + if ($target.data("snippet-editor")) { $target.data("snippet-editor").drop_and_build_snippet($target); } @@ -439,9 +437,6 @@ },0); } else { $toInsert.remove(); - if (self.$modal.find('input:not(:checked)').length) { - self.$modal.modal('toggle'); - } } }, }); @@ -1126,6 +1121,38 @@ return this.grid; }, + onFocus : function () { + this._super(); + this.change_cursor(); + }, + + change_cursor : function () { + var _class = this.$target.attr("class") || ""; + + var col = _class.match(/col-md-([0-9-]+)/i); + col = col ? +col[1] : 0; + + var offset = _class.match(/col-md-offset-([0-9-]+)/i); + offset = offset ? +offset[1] : 0; + + var overlay_class = this.$overlay.attr("class").replace(/(^|\s+)block-[^\s]*/gi, ''); + if (col+offset >= 12) overlay_class+= " block-e-right"; + if (col === 1) overlay_class+= " block-w-right block-e-left"; + if (offset === 0) overlay_class+= " block-w-left"; + + var mb = _class.match(/mb([0-9-]+)/i); + mb = mb ? +mb[1] : 0; + if (mb >= 128) overlay_class+= " block-s-bottom"; + else if (!mb) overlay_class+= " block-s-top"; + + var mt = _class.match(/mt([0-9-]+)/i); + mt = mt ? +mt[1] : 0; + if (mt >= 128) overlay_class+= " block-n-top"; + else if (!mt) overlay_class+= " block-n-bottom"; + + this.$overlay.attr("class", overlay_class); + }, + /* on_resize * called when the box is resizing and the class change, before the cover_target * @compass: resize direction : 'n', 's', 'e', 'w' @@ -1133,7 +1160,7 @@ * @current: curent increment in this.grid */ on_resize: function (compass, beginClass, current) { - + this.change_cursor(); } }); @@ -1212,27 +1239,27 @@ return false; }, on_resize: function (compass, beginClass, current) { - if (compass !== 'w') - return; - - // don't change the right border position when we change the offset (replace col size) - var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0); - var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0); - var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0); - if (offset < 0) { - offset = 0; - } - var colSize = beginCol - (offset - beginOffset); - if (colSize <= 0) { - colSize = 1; - offset = beginOffset + beginCol - 1; - } - this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, '')); + if (compass === 'w') { + // don't change the right border position when we change the offset (replace col size) + var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0); + var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0); + var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0); + if (offset < 0) { + offset = 0; + } + var colSize = beginCol - (offset - beginOffset); + if (colSize <= 0) { + colSize = 1; + offset = beginOffset + beginCol - 1; + } + this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, '')); - this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize)); - if (offset > 0) { - this.$target.addClass('col-md-offset-' + offset); + this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize)); + if (offset > 0) { + this.$target.addClass('col-md-offset-' + offset); + } } + this._super(compass, beginClass, current); }, }); @@ -1254,8 +1281,6 @@ this.$target.attr('contentEditable', 'false'); this.$target.find('.oe_structure, .content>.row').attr('contentEditable', 'true'); - - this.$target.carousel('pause'); }, clean_for_save: function () { this._super(); @@ -1264,14 +1289,6 @@ this.$target.find(".item:first").addClass("active"); } }, - onFocus: function () { - this._super(); - this.$target.carousel('pause'); - }, - onBlur: function () { - this._super(); - this.$target.carousel('cycle'); - }, start : function () { var self = this; this._super(); @@ -1282,6 +1299,7 @@ this.$editor.find(".js_add").on('click', function () {self.on_add_slide(); return false;}); this.$editor.find(".js_remove").on('click', function () {self.on_remove_slide(); return false;}); + this.$target.carousel('pause'); this.rebind_event(); }, on_add_slide: function () { @@ -1401,7 +1419,7 @@ this.$target.find('.carousel-control').off('click').on('click', function () { self.$target.carousel( $(this).data('slide')); }); - this.$target.find('.carousel-image, .content').attr('contentEditable', 'true'); + this.$target.find('.carousel-inner .content > div').attr('contentEditable', 'true'); this.$target.find('.carousel-image').attr('attributeEditable', 'true'); this._super(); }, diff --git a/addons/website/static/src/js/website.tour.banner.js b/addons/website/static/src/js/website.tour.banner.js index 7d307bd26d7..53489b1737c 100644 --- a/addons/website/static/src/js/website.tour.banner.js +++ b/addons/website/static/src/js/website.tour.banner.js @@ -100,8 +100,9 @@ { element: '.modal:has(#mobile-viewport) button[data-dismiss=modal]', placement: 'right', - title: _t("Close Mobile Preview"), + title: _t("Check Mobile Preview"), content: _t("Scroll in the mobile preview to test the rendering. Once it's ok, close this dialog."), + popover: { next: _t("Continue") }, }, { waitNot: '.modal', diff --git a/addons/website/static/src/js/website.tour.js b/addons/website/static/src/js/website.tour.js index f1d5ab79228..2416cb511f1 100644 --- a/addons/website/static/src/js/website.tour.js +++ b/addons/website/static/src/js/website.tour.js @@ -281,6 +281,14 @@ website.Tour = openerp.Class.extend({ self.nextStep(step.stepId, callback, overlaps); }, self.defaultDelay); } else if (!overlaps || new Date().getTime() - time < overlaps) { + if (self.current.element) { + var $popover = $(".popover.tour"); + if(!$(self.current.element).is(":visible")) { + $popover.data("hide", true).fadeOut(300); + } else if($popover.data("hide")) { + $popover.data("hide", false).fadeIn(150); + } + } self.timer = setTimeout(checkNext, self.defaultDelay); } else { self.reset(); diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/website/static/src/xml/website.snippets.xml index cf3996bdba4..d73e0c3934a 100644 --- a/addons/website/static/src/xml/website.snippets.xml +++ b/addons/website/static/src/xml/website.snippets.xml @@ -15,43 +15,6 @@ </div> </li> </t> - <t t-name="website.snippets_modal"> - <div class="modal" id="oe_droperror" tabindex="-1" role="dialog"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal">&times;</button> - <h4 class="modal-title"> - Building Block Not Inserted - </h4> - </div> - <div class="modal-body"> - <p> - Please drop building blocks in a valid zone, like - this one: - </p> - <div class="row"> - <div class="col-sm-4 col-sm-offset-4 oe_drop_zone_style text-center"> - <span class="mt16">drop in this zone</span> - </div> - </div> - <div class="text-muted"> - <b>Tip:</b> when your cursor is in a valid zone, - OpenERP shows you a preview of the building block - in the page. You must release the block when the - preview is visible. - </div> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button> - <div class="pull-left"> - <input type="checkbox" value="1"/> Never show this tip again - </div> - </div> - </div> - </div> - </div> - </t> <t t-name="website.snippets"> <div id='oe_snippets'></div> diff --git a/addons/website/static/src/xml/website.xml b/addons/website/static/src/xml/website.xml index f25a491a106..4d9c9fcd73d 100644 --- a/addons/website/static/src/xml/website.xml +++ b/addons/website/static/src/xml/website.xml @@ -74,7 +74,7 @@ </div> <div class="modal-body" t-if="message or backend_url"> <section> - <t t-esc="message"/> + <t t-raw="message"/> </section> <section class="mt32" t-if="backend_url"> <p>The web site has encountered an error.</p> diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index 16cbaf9d066..50d5d8e616c 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -34,11 +34,13 @@ <div class="item image_text active" style="background-image: url('/website/static/src/img/banner/color_splash.jpg')"> <div class="container"> <div class="carousel-caption content"> - <h2>Your Banner Title</h2> - <h3>Click to customize this text</h3> - <p> - <a href="/page/website.contactus" class="btn btn-success btn-large">Contact us</a> - </p> + <div> + <h2>Your Banner Title</h2> + <h3>Click to customize this text</h3> + <p> + <a href="/page/website.contactus" class="btn btn-success btn-large">Contact us</a> + </p> + </div> </div> <img class="carousel-image hidden-xs" src="/website/static/src/img/banner/banner_picture.png" alt="Banner OpenERP Image"/> </div> @@ -182,15 +184,6 @@ </div> - <div data-snippet-id="hr" data-selector-children=".oe_structure, [data-oe-type=html]"> - <div class="oe_snippet_thumbnail"> - <img class="oe_snippet_thumbnail_img" src="/website/static/src/img/blocks/block_separator.png"/> - <span class="oe_snippet_thumbnail_title">Separator</span> - </div> - <hr class="oe_snippet_body"/> - </div> - - <div data-snippet-id="big-picture" data-selector-children=".oe_structure, [data-oe-type=html]"> <div class="oe_snippet_thumbnail"> <img class="oe_snippet_thumbnail_img" src="/website/static/src/img/blocks/block_big_picture.png"/> @@ -372,6 +365,15 @@ </div> </div> + + <div data-snippet-id="hr" data-selector-children=".oe_structure, [data-oe-type=html]"> + <div class="oe_snippet_thumbnail"> + <img class="oe_snippet_thumbnail_img" src="/website/static/src/img/blocks/block_separator.png"/> + <span class="oe_snippet_thumbnail_title">Separator</span> + </div> + <hr class="oe_snippet_body"/> + </div> + </div> <div id="snippet_feature" class="tab-pane fade"> diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 8beff820baf..88cb65b9daa 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -69,7 +69,6 @@ <script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script> <script type="text/javascript" src="/web/static/lib/underscore.string/lib/underscore.string.js"></script> <script type="text/javascript" src="/web/static/lib/jquery/jquery.js"></script> - <script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script> <script type="text/javascript"> // Bootstrap and jQuery UI conflicts $.fn.bstooltip = $.fn.tooltip; @@ -80,6 +79,8 @@ <script type="text/javascript" src="/web/static/src/js/openerpframework.js"></script> <script type="text/javascript" src="/website/static/src/js/website.js"></script> + + <script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script> <script t-if="not translatable" type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script> <t t-raw="head or ''" name='layout_head'/> @@ -140,8 +141,8 @@ <li><a href="/page/website.contactus">Contact us</a></li> </ul> <ul class="list-unstyled"> - <li><i class="fa fa-phone"></i> <span t-field="res_company.phone"></span></li> - <li><i class="fa fa-envelope"></i> <span t-field="res_company.email"></span></li> + <li t-ignore="true"><i class="fa fa-phone"></i> <span t-field="res_company.phone"></span></li> + <li t-ignore="true"><i class="fa fa-envelope"></i> <span t-field="res_company.email"></span></li> </ul> <h2> <a t-att-href="website.social_facebook" t-if="website.social_facebook"><i class="fa fa-facebook-square"/></a> @@ -249,7 +250,7 @@ </div> </div> </xpath> - <xpath expr='//t[@name="layout_head"]' position="before"> + <xpath expr='//script[@src="/website/static/lib/bootstrap/js/bootstrap.js"]' position="before"> <link rel='stylesheet' href='/website/static/src/css/snippets.css'/> <link rel='stylesheet' href='/website/static/src/css/editor.css'/> <link rel='stylesheet' href='/website/static/lib/bootstrap-tour/bootstrap-tour.css'/> @@ -261,10 +262,10 @@ <script type="text/javascript" src="/web/static/lib/select2/select2.js"></script> <script type="text/javascript" src="/web/static/lib/ckeditor/ckeditor.js"></script> - <script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script> <script t-if="not translatable" type="text/javascript" src="/website/static/lib/ace/ace.js"></script> <script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script> <script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js"></script> + <script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script> <!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) --> <script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script> <script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script> @@ -383,13 +384,13 @@ <template id="pager" name="Pager"> <ul t-if="pager['page_count'] > 1" t-attf-class="#{ classname or '' } pagination" t-att-style="style or ''"> <li t-att-class=" 'disabled' if pager['page']['num'] == 1 else '' "> - <a t-att-href=" pager['page_previous']['url'] if pager['page']['num'] != 1 else '' ">Prev</a> + <a t-att-href=" pager['page_previous']['url'] if pager['page']['num'] != 1 else ''">Prev</a> </li> <t t-foreach="pager['pages']" t-as="page"> <li t-att-class=" 'active' if page['num'] == pager['page']['num'] else '' "> <a t-att-href="page['url']" t-raw="page['num']"></a></li> </t> <li t-att-class=" 'disabled' if pager['page']['num'] == pager['page_count'] else '' "> - <a t-att-href=" pager['page_next']['url'] if pager['page']['num'] != pager['page_count'] else '' ">Next</a> + <a t-att-href="pager['page_next']['url'] if pager['page']['num'] != pager['page_count'] else ''">Next</a> </li> </ul> </template> @@ -673,10 +674,10 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml <template id="company_description" name="Company Description"> <address itemscope="itemscope" itemtype="http://schema.org/Organization"> - <div itemprop="name" t-field="res_company.name">Name</div> - <br /> - <div><i class="fa fa-phone"/> <span itemprop="telephone" t-field="res_company.phone"/></div> - <div><i class="fa fa-envelope"/> <span itemprop="email" t-field="res_company.email"/></div> + <!-- TODO widget contact must add itemprop attributes --> + <div t-field="res_company.partner_id" t-field-options='{ + "widget": "contact", + "fields": ["name", "address", "phone", "mobile", "fax", "email"]}'/> </address> <a t-att-href="res_company.google_map_link()" target="_BLANK"> <img class="thumbnail img-responsive" t-att-src="res_company.google_map_img()" /> @@ -703,7 +704,7 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml </div> <div class="col-md-4 mb32"> <div groups="base.group_website_publisher" t-ignore="true" class="pull-right css_editable_mode_hidden" t-att-style="style or ''"> - <a class="btn btn-primary" t-att-href="'/web#return_label=Website&model=%s&id=%s' % (res_company._name, res_company.id)" title='Edit in backend'>Edit</a> + <a class="btn btm-sm btn-default" t-att-href="'/web#return_label=Website&model=%s&id=%s' % (res_company._name, res_company.id)">Change address</a> </div> <t t-call="website.company_description"/> </div> diff --git a/addons/website_blog/static/description/index.html b/addons/website_blog/static/description/index.html index 7ca5b48ae55..3f44eea9b4d 100644 --- a/addons/website_blog/static/description/index.html +++ b/addons/website_blog/static/description/index.html @@ -22,7 +22,7 @@ translated in mutiple languages in just a few clicks. </p> <div class="oe_centeralign oe_websiteonly"> - <a href="http://www.openerp.com/start?app=website_sale" class="oe_button oe_big oe_tacky">Start your <span class="oe_emph">free</span> online store</a> + <a href="http://www.openerp.com/start?app=website_blog" class="oe_button oe_big oe_tacky">Start your <span class="oe_emph">free</span> online store</a> </div> </div> </div> diff --git a/addons/website_blog/static/src/js/website.tour.blog.js b/addons/website_blog/static/src/js/website.tour.blog.js index b6701d74634..e76e793dfda 100644 --- a/addons/website_blog/static/src/js/website.tour.blog.js +++ b/addons/website_blog/static/src/js/website.tour.blog.js @@ -27,7 +27,7 @@ element: '#content-menu-button', placement: 'left', title: _t("Add Content"), - content: _t("Create new pages, blogs, menu items and products through the <em>'Content'</em> menu."), + content: _t("Use this <em>'Content'</em> menu to create a new blog post like any other document (page, menu, products, event, ...)."), popover: { fixed: true }, }, { @@ -59,7 +59,7 @@ { waitNot: '#wrap h1[data-oe-model="blog.post"]:contains("Blog Post Title")', element: 'button[data-action=snippet]', - placement: 'bottom', + placement: 'left', title: _t("Layout Your Blog Post"), content: _t("Use well designed building blocks to structure the content of your blog. Click 'Insert Blocks' to add new content."), popover: { fixed: true }, @@ -68,7 +68,7 @@ snippet: 'image-text', placement: 'bottom', title: _t("Drag & Drop a Block"), - content: _t("Drag the <em>'Image-Text'</em> block and drop it in your page."), + content: _t("Drag this block and drop it in your page."), popover: { fixed: true }, }, { @@ -82,14 +82,14 @@ snippet: 'text-block', placement: 'bottom', title: _t("Drag & Drop a block"), - content: _t("Drag the <em>'Text Block'</em> block and drop it below the image block."), + content: _t("Drag this block and drop it below the image block."), popover: { fixed: true }, }, { element: '.oe_active .oe_snippet_remove', placement: 'top', - title: _t("Delete the Title"), - content: _t("From this toolbar you can move, duplicate or delete the selected zone. Click on the garbage can image to delete the title."), + title: _t("Delete the block"), + content: _t("From this toolbar you can move, duplicate or delete the selected zone. Click on the garbage can image to delete the block. Or click on the Title and delete it."), }, { waitNot: '.oe_active .oe_snippet_remove:visible', diff --git a/addons/website_event/static/src/js/website.tour.event.js b/addons/website_event/static/src/js/website.tour.event.js index 73510b455dd..a9e40a5f65b 100644 --- a/addons/website_event/static/src/js/website.tour.event.js +++ b/addons/website_event/static/src/js/website.tour.event.js @@ -21,7 +21,7 @@ { title: _t("Create an Event"), content: _t("Let's go through the first steps to publish a new event."), - popover: { next: _("Start Tutorial"), end: _("Skip It") }, + popover: { next: _t("Start Tutorial"), end: _t("Skip It") }, }, { element: '#content-menu-button', diff --git a/addons/website_livechat/__openerp__.py b/addons/website_livechat/__openerp__.py index 0ab7781b7ab..5220fc47bb6 100644 --- a/addons/website_livechat/__openerp__.py +++ b/addons/website_livechat/__openerp__.py @@ -12,10 +12,7 @@ OpenERP Website LiveChat 'depends': ['website', 'im_livechat'], 'installable': True, 'data': [ - 'views/website_livechat.xml' + 'views/website_livechat.xml', + 'views/res_config.xml' ], - 'demo': [ ], - 'js': [], - 'qweb': [], - 'css': [], } diff --git a/addons/website_livechat/models/website.py b/addons/website_livechat/models/website.py index 7e86042a67a..3e3df1c5bdc 100644 --- a/addons/website_livechat/models/website.py +++ b/addons/website_livechat/models/website.py @@ -5,4 +5,11 @@ class website(osv.osv): _columns = { 'channel_id': fields.many2one('im_livechat.channel', string="Channel"), - }
\ No newline at end of file + } + +class website_config_settings(osv.osv_memory): + _inherit = 'website.config.settings' + + _columns = { + 'channel_id': fields.related('website_id', 'channel_id', type='many2one', relation='im_livechat.channel', string='Live Chat Channel'), + } diff --git a/addons/website_livechat/views/res_config.xml b/addons/website_livechat/views/res_config.xml new file mode 100644 index 00000000000..ac69929ca63 --- /dev/null +++ b/addons/website_livechat/views/res_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data> + + <record id="view_website_config_settings" model="ir.ui.view"> + <field name="name">Website livechat settings</field> + <field name="model">website.config.settings</field> + <field name="inherit_id" ref="website.view_website_config_settings"/> + <field name="arch" type="xml"> + <xpath expr="//group[@string='Social Media']" position="inside"> + <label for="channel_id"/> + <div name="channel_id"> + <div class="oe_inline"> + <field name="channel_id"/> + </div> + </div> + </xpath> + </field> + </record> + + </data> +</openerp> diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml index d4da07f2bf0..374e0d5dd16 100644 --- a/addons/website_livechat/views/website_livechat.xml +++ b/addons/website_livechat/views/website_livechat.xml @@ -2,22 +2,6 @@ <openerp> <data> -<record id="website_settings_livechat" model="ir.ui.view"> - <field name="name">website.form</field> - <field name="model">website</field> - <field name="inherit_id" ref="website.view_website_form"/> - <field name="arch" type="xml"> - <xpath expr="//div[@name='other']" position='before'> - <div name="livechat"> - <separator string="LiveChat"/> - <group name="livechat"> - <field name="channel_id" widget="many2one"/> - </group> - </div> - </xpath> - </field> -</record> - <template id="header" inherit_id="website.layout" name="LiveChat Snippet"> <xpath expr="//body" position="inside"> <t t-if="website.channel_id"> diff --git a/addons/website_mail/models/mail_message.py b/addons/website_mail/models/mail_message.py index 308cf9da4b8..c708d9c3e33 100644 --- a/addons/website_mail/models/mail_message.py +++ b/addons/website_mail/models/mail_message.py @@ -28,13 +28,21 @@ class MailMessage(osv.Model): _columns = { 'website_published': fields.boolean( - 'Publish', help="Publish on the website as a blog" + 'Published', help="Visible on the website as a comment" ), } - _defaults = { - 'website_published': True, - } + def default_get(self, cr, uid, fields_list, context=None): + defaults = super(MailMessage, self).default_get(cr, uid, fields_list, context=context) + + # Note: explicitly implemented in default_get() instead of _defaults, + # to avoid setting to True for all existing messages during upgrades. + # TODO: this default should probably be dynamic according to the model + # on which the messages are attached, thus moved to create(). + if 'website_published' in fields_list: + defaults.setdefault('website_published', True) + + return defaults def _search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 5e02def1494..3531853227c 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -7,6 +7,7 @@ from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request from openerp.tools.translate import _ +from openerp.addons.website.models.website import slug PPG = 20 # Products Per Page PPR = 4 # Products Per Row @@ -143,8 +144,8 @@ class Ecommerce(http.Controller): return key_val return False - @http.route(['/shop/filters/'], type='http', auth="public", website=True, multilang=True) - def filters(self, **post): + @http.route(['/shop/filters/'], type='http', auth="public", methods=['POST'], website=True, multilang=True) + def filters(self, category=None, **post): index = [] filters = [] for key, val in post.items(): @@ -169,11 +170,16 @@ class Ecommerce(http.Controller): filters[index.index(cat_id)].append( cat[2] ) post.pop(key) - return request.redirect("/shop/?filters=%s%s%s" % ( - simplejson.dumps(filters), - post.get("search") and ("&search=%s" % post.get("search")) or "", - post.get("category") and ("&category=%s" % post.get("category")) or "" - )) + url = "/shop/" + if category: + category_obj = request.registry.get('product.public.category') + url = "%scategory/%s/" % (url, slug(category_obj.browse(request.cr, request.uid, int(category), context=request.context))) + if filters: + url = "%s?filters=%s" % (url, simplejson.dumps(filters)) + if post.get("search"): + url = "%s%ssearch=%s" % (url, filters and "&" or "?", post.get("search")) + + return request.redirect(url) def attributes_to_ids(self, attributes): obj = request.registry.get('product.attribute.line') @@ -209,15 +215,24 @@ class Ecommerce(http.Controller): ('name', 'ilike', search), ('description', 'ilike', search)] if category: - domain.append(('product_variant_ids.public_categ_id', 'child_of', category.id)) + domain.append(('product_variant_ids.public_categ_id', 'child_of', int(category))) + if isinstance(category, (int,str,unicode)): + category = request.registry.get('product.public.category').browse(cr, uid, int(category), context=context) if filters: filters = simplejson.loads(filters) if filters: ids = self.attributes_to_ids(filters) domain.append(('id', 'in', ids or [0])) + url = "/shop/" product_count = product_obj.search_count(cr, uid, domain, context=context) - pager = request.website.pager(url="/shop/", total=product_count, page=page, step=PPG, scope=7, url_args=post) + if search: + post["search"] = search + if filters: + post["filters"] = filters + if category: + url = "/shop/category/%s/" % slug(category) + pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post) request.context['pricelist'] = self.get_pricelist() @@ -244,11 +259,12 @@ class Ecommerce(http.Controller): 'range': range, 'search': { 'search': search, - 'category': category and category.id, + 'category': category and int(category), 'filters': filters, }, 'pager': pager, 'styles': styles, + 'category': category, 'categories': categs, 'Ecommerce': self, # TODO fp: Should be removed 'style_in_product': lambda style, product: style.id in [s.id for s in product.website_style_ids], @@ -257,26 +273,20 @@ class Ecommerce(http.Controller): @http.route(['/shop/product/<model("product.template"):product>/'], type='http', auth="public", website=True, multilang=True) def product(self, product, search='', category='', filters='', **kwargs): - category_obj = request.registry.get('product.public.category') - - category_ids = category_obj.search(request.cr, request.uid, [], context=request.context) - category_list = category_obj.name_get(request.cr, request.uid, category_ids, context=request.context) - category_list = sorted(category_list, key=lambda category: category[1]) - if category: + category_obj = request.registry.get('product.public.category') category = category_obj.browse(request.cr, request.uid, int(category), context=request.context) request.context['pricelist'] = self.get_pricelist() values = { 'Ecommerce': self, - 'category': category, - 'category_list': category_list, 'main_object': product, 'product': product, + 'category': category, 'search': { 'search': search, - 'category': category and str(category.id), + 'category': category and int(category), 'filters': filters, } } diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml index 9918568a32d..abbcaa26ab9 100644 --- a/addons/website_sale/views/website_sale.xml +++ b/addons/website_sale/views/website_sale.xml @@ -32,10 +32,10 @@ <!-- List of categories --> <template id="categories_recursive" name="Category list"> - <li t-att-class="category.id == search.get('category') and 'active' or ''"> - <a t-attf-href="/shop/category/#{ slug(category) }/" t-field="category.name"></a> - <ul t-if="category.child_id" class="nav nav-pills nav-stacked nav-hierarchy"> - <t t-foreach="category.child_id" t-as="category"> + <li t-att-class="int(categ) == int(category or 0) and 'active' or ''"> + <a t-attf-href="/shop/category/#{ slug(categ) }/" t-field="categ.name"></a> + <ul t-if="categ.child_id" class="nav nav-pills nav-stacked nav-hierarchy"> + <t t-foreach="categ.child_id" t-as="categ"> <t t-call="website_sale.categories_recursive"/> </t> </ul> @@ -45,14 +45,15 @@ <!-- Product list --> <template id="search" name="Search hidden fields"> - <input type="hidden" name="category" t-att-value="search.get('category') or ''"/> - <input type="hidden" name="filters" t-att-value="search.get('filters') or ''"/> - <div class="input-group"> - <input type="text" name="search" class="search-query form-control" placeholder="Search..." t-att-value="search.get('search') or ''"/> - <span class="input-group-btn"> - <button class="btn btn-default" type="submit"><i class="fa fa-search"/></button> - </span> - </div> + <form t-attf-action="/shop/{{'category/%s/' % slug(category) if category else ''}}" method="get" t-att-class="search_class"> + <input t-if="search.get('filters')" type="hidden" name="filters" t-att-value="search.get('filters')"/> + <div class="input-group"> + <input type="text" name="search" class="search-query form-control" placeholder="Search..." t-att-value="search.get('search') or ''"/> + <span class="input-group-btn"> + <button class="btn btn-default" type="submit"><i class="fa fa-search"/></button> + </span> + </div> + </form> </template> <template id="products_cart" name="Shopping cart"> @@ -61,12 +62,12 @@ <div class="ribbon btn btn-danger">Sale</div> </div> <div class="oe_product_image"> - <a itemprop="url" t-attf-href="/shop/product/{{ slug(product) }}/?{{ keep_query('category', 'search', 'filters') }}"> + <a itemprop="url" t-attf-href="/shop/product/{{ slug(product) }}/?{{ keep_query('search', 'filters', category=(category and int(category)), page=(pager['page']['num'] if pager['page']['num']>1 else None)) }}"> <span itemprop="image" t-field="product.image" t-field-options='{"widget": "image"}'/> </a> </div> <section> - <h5><strong><a itemprop="name" t-attf-href="/shop/product/{{ slug(product) }}/?{{ keep_query('category', 'search', 'filters') }}" t-field="product.name"/></strong></h5> + <h5><strong><a itemprop="name" t-attf-href="/shop/product/{{ slug(product) }}/?{{ keep_query('search', 'filters', category=(category and int(category)), page=(pager['page']['num'] if pager['page']['num']>1 else None)) }}" t-field="product.name"/></strong></h5> <div itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer" class="product_price" t-if="product.product_variant_ids"> <b> <t t-if="abs(product.product_variant_ids[0].lst_price - product.product_variant_ids[0].price) > 0.2"> @@ -104,9 +105,8 @@ <div class="container oe_website_sale"> <div class="products_pager"> <div class="row"> - <form action="/shop/" method="get" class="pagination form-inline col-md-3"> - <t t-call="website_sale.search" /> - </form> + <t t-set="search_class">pagination form-inline col-md-3</t> + <t t-call="website_sale.search"/> <t t-call="website.pager"/> </div> </div> @@ -278,15 +278,14 @@ <div class="row"> <div class="col-sm-4"> <ol class="breadcrumb"> - <li><a t-attf-href="/shop?{{ keep_query('search', 'filters') }}" onclick="history.go(-1); return false;">Products</a></li> - <li t-if="search.get('category')"><a t-attf-href="/shop/?{{ keep_query('search', 'filters', 'category') }}" t-field="category.name"/></li> + <li><a t-attf-href="/shop?{{ keep_query('search', 'filters', 'page') if not category else keep_query('search', 'filters') }}">Products</a></li> + <li t-if="category"><a t-attf-href="/shop/category/{{ slug(category) }}/?{{ keep_query('search', 'filters', 'page') }}" t-field="category.name"/></li> <li class="active"><span t-field="product.name"/></li> </ol> </div> <div class="col-sm-3"> - <form action="/shop/" method="get" class="pull-right"> - <t t-call="website_sale.search" /> - </form> + <t t-set="search_class">pull-right</t> + <t t-call="website_sale.search"/> </div> <div class="col-sm-4" groups="base.group_sale_manager"> <t t-call="website.publish_management"> @@ -341,7 +340,7 @@ </div> <button class="btn btn-primary btn-lg mt8">Add to Cart</button> <hr t-if="product.description_sale"/> - <p t-field="product.description_sale" class="text-muted"/> + <div><p t-field="product.description_sale" class="text-muted"/></div> <hr/> <p class="text-muted"> 30-day money-back guarantee<br/> @@ -571,8 +570,8 @@ <template id="products_categories" inherit_option_id="website_sale.products" name="Product Categories"> <xpath expr="//div[@id='products_grid_before']" position="inside"> <ul class="nav nav-pills nav-stacked mt16"> - <li t-att-class=" '' if search.get('category') else 'active' "><a href="/shop/">All Products</a></li> - <t t-foreach="categories" t-as="category"> + <li t-att-class=" '' if category else 'active' "><a href="/shop/">All Products</a></li> + <t t-foreach="categories" t-as="categ"> <t t-call="website_sale.categories_recursive"/> </t> </ul> @@ -587,7 +586,7 @@ <template id="products_attributes" inherit_id="website_sale.products" inherit_option_id="website_sale.products" name="Product attribute's Filters" groups="product.group_product_attributes"> <xpath expr="//div[@id='products_grid_before']" position="inside"> - <form t-attf-action="/shop/filters/?{{ keep_query('category', 'search') }}" class="attributes" method="post"> + <form t-attf-action="/shop/filters/?{{ keep_query('search', category=(category and int(category))) }}" class="attributes" method="post"> <ul class="nav nav-pills nav-stacked mt16"> <t t-set="attribute_ids" t-value="Ecommerce.get_attribute_ids()"/> <t t-foreach="attribute_ids" t-as="attribute_id"> |
