[MERGE] forward port of branch saas-10 up to d73f0d8

This commit is contained in:
Christophe Simonis
2016-06-21 13:58:24 +02:00
43 changed files with 422 additions and 57 deletions
@@ -422,20 +422,28 @@ class AccountBankStatementLine(models.Model):
def button_cancel_reconciliation(self): def button_cancel_reconciliation(self):
moves_to_unbind = self.env['account.move'] moves_to_unbind = self.env['account.move']
moves_to_cancel = self.env['account.move'] moves_to_cancel = self.env['account.move']
payment_to_unreconcile = self.env['account.payment']
for st_line in self: for st_line in self:
moves_to_unbind |= st_line.journal_entry_ids moves_to_unbind |= st_line.journal_entry_ids
for move in st_line.journal_entry_ids: for move in st_line.journal_entry_ids:
if any(line.payment_id for line in move.line_ids): if any(line.payment_id for line in move.line_ids):
for line in move.line_ids:
payment_to_unreconcile |= line.payment_id
continue continue
moves_to_cancel |= st_line.journal_entry_ids moves_to_cancel |= st_line.journal_entry_ids
if moves_to_unbind: if moves_to_unbind:
moves_to_unbind.write({'statement_line_id': False}) moves_to_unbind.write({'statement_line_id': False})
moves_to_unbind.line_ids.filtered(lambda x:x.statement_id == st_line.statement_id).write({'statement_id': False})
if moves_to_cancel: if moves_to_cancel:
for move in moves_to_cancel: for move in moves_to_cancel:
move.line_ids.remove_move_reconcile() move.line_ids.remove_move_reconcile()
moves_to_cancel.button_cancel() moves_to_cancel.button_cancel()
moves_to_cancel.unlink() moves_to_cancel.unlink()
if payment_to_unreconcile:
payment_to_unreconcile.write({'state': 'posted'})
#################################################### ####################################################
# Reconciliation interface methods # Reconciliation interface methods
#################################################### ####################################################
@@ -567,7 +575,18 @@ class AccountBankStatementLine(models.Model):
else: else:
domain = [(f, '>', 0), (f, '<', amount)] domain = [(f, '>', 0), (f, '<', amount)]
elif comparator == '=': elif comparator == '=':
domain = [(f, '=', float_round(amount, precision_digits=p))] if f == 'amount_residual':
domain = [
'|', (f, '=', float_round(amount, precision_digits=p)),
'&', ('account_id.internal_type', '=', 'liquidity'),
'|', ('debit', '=', amount), ('credit', '=', amount),
]
else:
domain = [
'|', (f, '=', float_round(amount, precision_digits=p)),
'&', ('account_id.internal_type', '=', 'liquidity'),
('amount_currency', '=', amount),
]
else: else:
raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'")) raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'"))
domain += [('currency_id', '=', c)] domain += [('currency_id', '=', c)]
+3
View File
@@ -717,6 +717,9 @@ class AccountInvoice(models.Model):
line2[tmp]['credit'] = (am < 0) and -am or 0.0 line2[tmp]['credit'] = (am < 0) and -am or 0.0
line2[tmp]['amount_currency'] += l['amount_currency'] line2[tmp]['amount_currency'] += l['amount_currency']
line2[tmp]['analytic_line_ids'] += l['analytic_line_ids'] line2[tmp]['analytic_line_ids'] += l['analytic_line_ids']
qty = l.get('quantity')
if qty:
line2[tmp]['quantity'] = line2[tmp].get('quantity', 0.0) + qty
else: else:
line2[tmp] = l line2[tmp] = l
line = [] line = []
@@ -142,11 +142,22 @@ class account_journal(models.Model):
if self.type in ['bank', 'cash']: if self.type in ['bank', 'cash']:
last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)], order="date desc, id desc", limit=1) last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)], order="date desc, id desc", limit=1)
last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0 last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
ac_bnk_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids),('state', '=', 'open')]) #Get the number of items to reconcile for that bank journal
for ac_bnk in ac_bnk_stmt: self.env.cr.execute("""SELECT COUNT(DISTINCT(statement_line_id))
for line in ac_bnk.line_ids: FROM account_move where statement_line_id
if not line.journal_entry_ids: IN (SELECT line.id
number_to_reconcile += 1 FROM account_bank_statement_line AS line
LEFT JOIN account_bank_statement AS st
ON line.statement_id = st.id
WHERE st.journal_id IN %s and st.state = 'open')""", (tuple(self.ids),))
already_reconciled = self.env.cr.fetchone()[0]
self.env.cr.execute("""SELECT COUNT(line.id)
FROM account_bank_statement_line AS line
LEFT JOIN account_bank_statement AS st
ON line.statement_id = st.id
WHERE st.journal_id IN %s and st.state = 'open'""", (tuple(self.ids),))
all_lines = self.env.cr.fetchone()[0]
number_to_reconcile = all_lines - already_reconciled
# optimization to read sum of balance from account_move_line # optimization to read sum of balance from account_move_line
account_ids = tuple(filter(None, [self.default_debit_account_id.id, self.default_credit_account_id.id])) account_ids = tuple(filter(None, [self.default_debit_account_id.id, self.default_credit_account_id.id]))
if account_ids: if account_ids:
+24 -8
View File
@@ -583,7 +583,14 @@ class AccountMoveLine(models.Model):
] ]
try: try:
amount = float(str) amount = float(str)
amount_domain = ['|', ('amount_residual', '=', amount), '|', ('amount_residual_currency', '=', amount), '|', ('amount_residual', '=', -amount), ('amount_residual_currency', '=', -amount)] amount_domain = [
'|', ('amount_residual', '=', amount),
'|', ('amount_residual_currency', '=', amount),
'|', ('amount_residual', '=', -amount),
'|', ('amount_residual_currency', '=', -amount),
'&', ('account_id.internal_type', '=', 'liquidity'),
'|', '|', ('debit', '=', amount), ('credit', '=', amount), ('amount_currency', '=', amount),
]
str_domain = expression.OR([str_domain, amount_domain]) str_domain = expression.OR([str_domain, amount_domain])
except: except:
pass pass
@@ -1067,13 +1074,13 @@ class AccountMoveLine(models.Model):
if vals['debit'] != 0.0: vals['debit'] = res['total_excluded'] if vals['debit'] != 0.0: vals['debit'] = res['total_excluded']
if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded'] if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded']
if vals.get('amount_currency'): if vals.get('amount_currency'):
vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (amount / res['total_excluded'])) vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (res['total_excluded']/amount))
# Create tax lines # Create tax lines
for tax_vals in res['taxes']: for tax_vals in res['taxes']:
if tax_vals['amount']: if tax_vals['amount']:
account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id']) account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id'])
if not account_id: account_id = vals['account_id'] if not account_id: account_id = vals['account_id']
tax_lines_vals.append({ temp = {
'account_id': account_id, 'account_id': account_id,
'name': vals['name'] + ' ' + tax_vals['name'], 'name': vals['name'] + ' ' + tax_vals['name'],
'tax_line_id': tax_vals['id'], 'tax_line_id': tax_vals['id'],
@@ -1082,7 +1089,15 @@ class AccountMoveLine(models.Model):
'statement_id': vals.get('statement_id'), 'statement_id': vals.get('statement_id'),
'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0, 'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0,
'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0, 'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0,
}) }
bank = self.env["account.bank.statement"].browse(vals.get('statement_id'))
if bank.currency_id != bank.company_id.currency_id:
ctx = {}
if 'date' in vals:
ctx['date'] = vals['date']
temp['currency_id'] = bank.currency_id.id
temp['amount_currency'] = bank.company_id.currency_id.with_context(ctx).compute(tax_vals['amount'], bank.currency_id, round=True)
tax_lines_vals.append(temp)
new_line = super(AccountMoveLine, self).create(vals) new_line = super(AccountMoveLine, self).create(vals)
for tax_line_vals in tax_lines_vals: for tax_line_vals in tax_lines_vals:
@@ -1124,10 +1139,11 @@ class AccountMoveLine(models.Model):
msg = _('New expected payment date: ') + vals['expected_pay_date'] + '.\n' + vals.get('internal_note', '') msg = _('New expected payment date: ') + vals['expected_pay_date'] + '.\n' + vals.get('internal_note', '')
self.invoice_id.message_post(body=msg) #TODO: check it is an internal note (not a regular email)! self.invoice_id.message_post(body=msg) #TODO: check it is an internal note (not a regular email)!
#when making a reconciliation on an existing liquidity journal item, mark the payment as reconciled #when making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
if 'statement_id' in vals and self.payment_id: for record in self:
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement if 'statement_id' in vals and record.payment_id:
if all(line.statement_id for line in self.payment_id.move_line_ids.filtered(lambda r: r.id != self.id and r.account_id.internal_type=='liquidity')): # In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
self.payment_id.state = 'reconciled' if all(line.statement_id for line in record.payment_id.move_line_ids.filtered(lambda r: r.id != record.id and r.account_id.internal_type=='liquidity')):
record.payment_id.state = 'reconciled'
result = super(AccountMoveLine, self).write(vals) result = super(AccountMoveLine, self).write(vals)
if self._context.get('check_move_validity', True): if self._context.get('check_move_validity', True):
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime
import time import time
from openerp import api, models from openerp import api, models
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
class ReportPartnerLedger(models.AbstractModel): class ReportPartnerLedger(models.AbstractModel):
@@ -26,7 +28,12 @@ class ReportPartnerLedger(models.AbstractModel):
self.env.cr.execute(query, tuple(params)) self.env.cr.execute(query, tuple(params))
res = self.env.cr.dictfetchall() res = self.env.cr.dictfetchall()
sum = 0.0 sum = 0.0
lang_code = self.env.context.get('lang') or 'en_US'
lang = self.env['res.lang']
lang_id = lang._lang_get(lang_code)
date_format = lang.browse(lang_id).date_format
for r in res: for r in res:
r['date'] = datetime.strptime(r['date'], DEFAULT_SERVER_DATE_FORMAT).strftime(date_format)
r['displayed_name'] = '-'.join( r['displayed_name'] = '-'.join(
r[field_name] for field_name in ('move_name', 'ref', 'name') r[field_name] for field_name in ('move_name', 'ref', 'name')
if r[field_name] not in (None, '', '/') if r[field_name] not in (None, '', '/')
@@ -36,7 +36,9 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
if (match) { if (match) {
var barcode = match[1]; var barcode = match[1];
core.bus.trigger('barcode_scanned', barcode); // Send the target in case there are several barcode widgets on the same page (e.g.
// registering the lot numbers in a stock picking)
core.bus.trigger('barcode_scanned', barcode, this.buffered_key_events[0].target);
// Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set. // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set.
if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true") if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true")
@@ -13,8 +13,13 @@ return {
init: function() { init: function() {
var self = this; var self = this;
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.__on_barcode_scanned = function () { this.__on_barcode_scanned = function (barcode, target) {
self.on_barcode_scanned.apply(self, arguments); // Handle the case where there are several barcode widgets on the same page. Since the
// event is global on the page, all barcode widgets will be triggered. However, we only
// want to keep the event on the target widget.
if ($.contains(target, self.el)) {
self.on_barcode_scanned.call(self, barcode);
}
}; };
this.start_listening(); this.start_listening();
// Handlers inside a View managed by a ViewManager only listen to barcode events while their view is displayed // Handlers inside a View managed by a ViewManager only listen to barcode events while their view is displayed
+1 -1
View File
@@ -20,7 +20,7 @@ class crm_stage(osv.Model):
_name = "crm.stage" _name = "crm.stage"
_description = "Stage of case" _description = "Stage of case"
_rec_name = 'name' _rec_name = 'name'
_order = "sequence" _order = "sequence, name, id"
_columns = { _columns = {
'name': fields.char('Stage Name', required=True, translate=True), 'name': fields.char('Stage Name', required=True, translate=True),
+1 -1
View File
@@ -478,7 +478,7 @@ class MassMailing(osv.Model):
'create_date': fields.datetime('Creation Date'), 'create_date': fields.datetime('Creation Date'),
'sent_date': fields.datetime('Sent Date', oldname='date', copy=False), 'sent_date': fields.datetime('Sent Date', oldname='date', copy=False),
'schedule_date': fields.datetime('Schedule in the Future'), 'schedule_date': fields.datetime('Schedule in the Future'),
'body_html': fields.html('Body', translate=True), 'body_html': fields.html('Body', translate=True, sanitize=False),
'attachment_ids': fields.many2many( 'attachment_ids': fields.many2many(
'ir.attachment', 'mass_mailing_ir_attachments_rel', 'ir.attachment', 'mass_mailing_ir_attachments_rel',
'mass_mailing_id', 'attachment_id', 'Attachments' 'mass_mailing_id', 'attachment_id', 'Attachments'
@@ -41,7 +41,8 @@
<field name="price_unit" widget="monetary"/> <field name="price_unit" widget="monetary"/>
<field name="price_subtotal" invisible="1" widget="monetary"/> <field name="price_subtotal" invisible="1" widget="monetary"/>
<field name="price_subtotal_incl" invisible="1" widget="monetary"/> <field name="price_subtotal_incl" invisible="1" widget="monetary"/>
<field name="tax_ids" widget="many2many_tags"/> <field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
<field name="tax_ids" invisible="1"/>
<field name="notice"/> <field name="notice"/>
</group> </group>
</form> </form>
+3 -2
View File
@@ -716,8 +716,9 @@ class product_template(osv.osv):
def copy(self, cr, uid, id, default=None, context=None): def copy(self, cr, uid, id, default=None, context=None):
if default is None: if default is None:
default = {} default = {}
template = self.browse(cr, uid, id, context=context) if 'name' not in default:
default['name'] = _("%s (copy)") % (template['name']) template = self.browse(cr, uid, id, context=context)
default['name'] = _("%s (copy)") % (template['name'])
return super(product_template, self).copy(cr, uid, id, default=default, context=context) return super(product_template, self).copy(cr, uid, id, default=default, context=context)
_defaults = { _defaults = {
+1 -1
View File
@@ -98,7 +98,7 @@ class SaleOrder(models.Model):
for order in self: for order in self:
order.order_line._compute_tax_id() order.order_line._compute_tax_id()
name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, index=True, default=lambda self: _('New')) name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New'))
origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.") origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.")
client_order_ref = fields.Char(string='Customer Reference', copy=False) client_order_ref = fields.Char(string='Customer Reference', copy=False)
+1
View File
@@ -214,6 +214,7 @@ class PushedFlow(models.Model):
'propagate': self.propagate, 'propagate': self.propagate,
'push_rule_id': self.id, 'push_rule_id': self.id,
'warehouse_id': self.warehouse_id.id, 'warehouse_id': self.warehouse_id.id,
'procurement_id': False,
}) })
move.write({'move_dest_id': new_move.id}) move.write({'move_dest_id': new_move.id})
new_move.action_confirm() new_move.action_confirm()
+2 -1
View File
@@ -37,6 +37,7 @@ from openerp.modules import get_resource_path
from openerp.tools import topological_sort from openerp.tools import topological_sort
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.tools import ustr from openerp.tools import ustr
from openerp.tools.misc import str2bool
from openerp import http from openerp import http
from openerp.http import request, serialize_exception as _serialize_exception from openerp.http import request, serialize_exception as _serialize_exception
from openerp.exceptions import AccessError from openerp.exceptions import AccessError
@@ -695,7 +696,7 @@ class Database(http.Controller):
def restore(self, master_pwd, backup_file, name, copy=False): def restore(self, master_pwd, backup_file, name, copy=False):
try: try:
data = base64.b64encode(backup_file.read()) data = base64.b64encode(backup_file.read())
request.session.proxy("db").restore(master_pwd, name, data, copy) request.session.proxy("db").restore(master_pwd, name, data, str2bool(copy))
return http.local_redirect('/web/database/manager') return http.local_redirect('/web/database/manager')
except Exception, e: except Exception, e:
error = "Database restore error: %s" % e error = "Database restore error: %s" % e
+1 -1
View File
@@ -241,7 +241,7 @@ with directly by external objects or search view controls
.. js:class:: openerp.web.search.Facet .. js:class:: openerp.web.search.Facet
A `backbone model`_ representing a single facet of the current A `backbone model`_ representing a single facet of the current
research. May map to a search field, or to a more complex or search. May map to a search field, or to a more complex or
fuzzier input (e.g. a custom filter or an advanced search). fuzzier input (e.g. a custom filter or an advanced search).
.. js:attribute:: category .. js:attribute:: category
+44 -14
View File
@@ -66,10 +66,45 @@ Tour.register({
}, },
{ {
title: "check the onchange from the o2m to the m2m", title: "check the onchange from the o2m to the m2m",
waitFor: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr td:contains(user_test)', waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr td:contains(user_test)',
},
// add ourself as participant
{
title: "change tab to Participants",
element: '[data-toggle="tab"]:contains(Participants)'
},
{
title: "click to add participants",
element: '.tab-pane:eq(2).active .o_form_field_x2many_list_row_add a'
},
{
title: "select Admin",
element: '.modal tr:has(td[data-field="name"]:containsExact(Administrator)) .o_list_record_selector input[type="checkbox"]'
},
{
title: "save selected participants",
waitFor: '.modal tr:has(td[data-field="name"]:containsExact(Administrator)) .o_list_record_selector input[type="checkbox"]:propChecked',
element: '.o_selectcreatepopup_search_select'
},
// save
{
title: "save discussion",
waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr:has(td:containsExact(Administrator))',
element: 'button.o_form_button_save'
},
// edit
{
title: "edit discussion",
element: 'button.o_form_button_edit'
}, },
// add message a // add message a
{
title: "Select First Tab",
element: 'a[role=tab]:first',
},
{ {
title: "create new message a", title: "create new message a",
element: '.tab-pane:eq(0) .o_form_field_x2many_list_row_add a' element: '.tab-pane:eq(0) .o_form_field_x2many_list_row_add a'
@@ -167,15 +202,10 @@ Tour.register({
}, },
{ {
title: "click to add participants", title: "click to add participants",
element: '.tab-pane:eq(1).active .o_form_field_x2many_list_row_add a' element: '.tab-pane:eq(2).active .o_form_field_x2many_list_row_add a'
},
{
title: "select Admin",
element: '.modal tr:has(td[data-field="name"]:containsExact(Administrator)) .o_list_record_selector input[type="checkbox"]'
}, },
{ {
title: "select Demo User", title: "select Demo User",
waitFor: '.modal tr:has(td[data-field="name"]:containsExact(Administrator)) .o_list_record_selector input[type="checkbox"]:propChecked',
element: '.modal tr:has(td[data-field="name"]:containsExact(Demo User)) .o_list_record_selector input[type="checkbox"]' element: '.modal tr:has(td[data-field="name"]:containsExact(Demo User)) .o_list_record_selector input[type="checkbox"]'
}, },
{ {
@@ -187,8 +217,8 @@ Tour.register({
// save // save
{ {
title: "save discussion", title: "save discussion",
waitFor: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)', waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)',
waitNot: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(3)', waitNot: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(3)',
element: 'button.o_form_button_save' element: 'button.o_form_button_save'
}, },
@@ -204,8 +234,8 @@ Tour.register({
}, },
{ {
title: "check data 3", title: "check data 3",
waitFor: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)', waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)',
waitNot: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(3)', waitNot: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(3)',
}, },
// edit // edit
@@ -214,7 +244,7 @@ Tour.register({
element: 'button.o_form_button_edit' element: 'button.o_form_button_edit'
}, },
{ {
title: "change tab to Participants", title: "change tab to Messages",
waitFor: '.o_form_editable', waitFor: '.o_form_editable',
element: '[data-toggle="tab"]:contains(Messages)' element: '[data-toggle="tab"]:contains(Messages)'
}, },
@@ -344,8 +374,8 @@ Tour.register({
}, },
{ {
title: "check data 7", title: "check data 7",
waitFor: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)', waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)',
waitNot: '.tab-pane:eq(1) .o_form_field.o_view_manager_content tbody tr[date-id]:eq(3)', waitNot: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[date-id]:eq(3)',
}, },
// edit // edit
+12 -3
View File
@@ -209,10 +209,19 @@
<input id="name" type="text" name="name" class="form-control" required="required"/> <input id="name" type="text" name="name" class="form-control" required="required"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <label for="copy">This database might have been moved or copied.</label>
<p class="help-block">In order to avoid conflicts between databases, Odoo needs to know if this database was moved or copied.
If you don't know, answer "This database is a copy".</p>
<div class="radio">
<label> <label>
<input name="copy" type="checkbox" class="pull-right" value="1"> <input name="copy" type="radio" class="pull-right" value="true" checked="1">
<span>Generate a new database uuid.</span> This database is a copy
</label>
</div>
<div class="radio">
<label>
<input name="copy" type="radio" class="pull-right" value="false">
This database was moved
</label> </label>
</div> </div>
</div> </div>
@@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n" "Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-01-28 12:13+0000\n" "POT-Creation-Date: 2016-06-17 08:01+0000\n"
"PO-Revision-Date: 2016-01-28 12:13+0000\n" "PO-Revision-Date: 2016-06-17 08:01+0000\n"
"Last-Translator: <>\n" "Last-Translator: <>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -25,14 +25,21 @@ msgstr ""
#. module: web_settings_dashboard #. module: web_settings_dashboard
#. openerp-web #. openerp-web
#: code:addons/web_settings_dashboard/static/src/xml/dashboard.xml:49 #: code:addons/web_settings_dashboard/static/src/xml/dashboard.xml:56
#, python-format #, python-format
msgid "Active User" msgid "Active User"
msgstr "" msgstr ""
#. module: web_settings_dashboard #. module: web_settings_dashboard
#. openerp-web #. openerp-web
#: code:addons/web_settings_dashboard/static/src/xml/dashboard.xml:9 #: code:addons/web_settings_dashboard/static/src/xml/dashboard.xml:57
#, python-format
msgid "Active Users"
msgstr ""
#. module: web_settings_dashboard
#. openerp-web
#: code:addons/web_settings_dashboard/static/src/xml/dashboard.xml:42
#, python-format #, python-format
msgid "App store" msgid "App store"
msgstr "" msgstr ""
@@ -52,7 +52,9 @@
<t t-set="active_users" t-value="widget.data.active_users"></t> <t t-set="active_users" t-value="widget.data.active_users"></t>
<t t-set="pending_users" t-value="widget.data.pending_users"></t> <t t-set="pending_users" t-value="widget.data.pending_users"></t>
<t t-if="active_users"> <t t-if="active_users">
<t t-esc="active_users"></t> Active User<t t-if="active_users > 1">s</t> <t t-esc="active_users"></t>
<t t-if="active_users &lt;= 1"> Active User</t>
<t t-if="active_users &gt; 1">Active Users</t>
</t> </t>
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -157,7 +157,7 @@
<t t-foreach="channels" t-as="channel"> <t t-foreach="channels" t-as="channel">
<div class="media"> <div class="media">
<a class="pull-left" t-attf-href="/livechat/channel/#{ slug(channel)}"> <a class="pull-left" t-attf-href="/livechat/channel/#{ slug(channel)}">
<img t-att-src="'data:image/png;base64,' + channel.image_small" t-att-alt="channel.name" /> <img t-att-src="channel.image_small and ('data:image/png;base64,' + channel.image_small) or '/web/static/src/img/placeholder.png'" t-att-alt="channel.name" />
</a> </a>
<div class="media-body"> <div class="media-body">
<h4 class="media-heading"><t t-esc="channel.name"/></h4> <h4 class="media-heading"><t t-esc="channel.name"/></h4>
@@ -60,6 +60,7 @@ class website_account(http.Controller):
error_message = [] error_message = []
mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id"] mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id"]
optional_billing_fields = ["zipcode", "state_id", "vat", "street"]
# Validation # Validation
for field_name in mandatory_billing_fields: for field_name in mandatory_billing_fields:
@@ -86,4 +87,9 @@ class website_account(http.Controller):
if [err for err in error.values() if err == 'missing']: if [err for err in error.values() if err == 'missing']:
error_message.append(_('Some required fields are empty.')) error_message.append(_('Some required fields are empty.'))
unknown = [k for k in data.iterkeys() if k not in mandatory_billing_fields + optional_billing_fields]
if unknown:
error['common'] = 'Unknown field'
error_message.append("Unknown field '%s'" % ','.join(unknown))
return error, error_message return error, error_message
+17
View File
@@ -0,0 +1,17 @@
Austria, 2016-06-16
Seekda GmbH agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Holger Lausen holger.lausen@seekda.com https://github.com/holgerlausen
List of contributors:
Holger Lausen holger.lausen@seekda.com https://github.com/holgerlausen
Florian Kisser florian.kisser@seekda.com https://github.com/floriankisser
+20
View File
@@ -0,0 +1,20 @@
Canada, 2016-06-09
Microcom agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Martin Malorni mmalorni@gmail.com https://github.com/mmalorni
List of contributors:
- Vincent Coll vincentgagnnoncoll@gmail.com https://github.com/vincentcoll
- Stéphane Le Cornec stephane.lecornec@gmail.com https://github.com/coleste
- Eric Lemire elemire@users.noreply.github.com https://github.com/elemire
- Martin Malorni mmalorni@gmail.com https://github.com/mmalorni
- Gregory Savoie gs.microcom@gmail.com https://github.com/gregsavoie
- Noreddine Ben Jillali nbj.microcom@gmail.com https://github.com/nbj-microcom
+15
View File
@@ -0,0 +1,15 @@
Germany, 2016-05-02
RIDING BYTES agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Ramon Bartl, rb@ridingbytes.com https://github.com/ridingbytes
List of contributors:
Ramon Bartl, rb@ridingbytes.com https://github.com/ramonski
+16
View File
@@ -0,0 +1,16 @@
Italy, 2016-02-23
Telnet Servizi Srl agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Telnet Data github@telnetservizi.it https://github.com/telnetdata
List of contributors:
Telnet Data github@telnetservizi.it https://github.com/telnetdata
Matteo Cantarutti cantarutti.matteo@live.it https://github.com/mteok
+11
View File
@@ -0,0 +1,11 @@
Finland, 2016-02-18
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Jarmo Kortetjärvi jarmo.kortetjarvi@gmail.com https://github.com/jarmokortetjarvi
+11
View File
@@ -0,0 +1,11 @@
Thailand, 2016-03-26
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Ross Golder ross@golder.org https://github.com/rossigee
+11
View File
@@ -0,0 +1,11 @@
Pakistan, 2016-05-05
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Sharjeel Ali Shaukat as_ns2002@hotmail.com https://github.com/salishaukat
+9
View File
@@ -0,0 +1,9 @@
Nicaragua, Jun 16th, 2016
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
William Moreno williamjmorenor@gmail.com https://github.com/williamjmorenor
+1 -1
View File
@@ -680,7 +680,7 @@ Model Reference
.. automethod:: read .. automethod:: read
.. automethod:: read_group .. automethod:: read_group
.. rubric:: Research .. rubric:: Searching
.. automethod:: search .. automethod:: search
.. automethod:: search_count .. automethod:: search_count
+1 -1
View File
@@ -822,7 +822,7 @@ attributes:
``default_group_by`` ``default_group_by``
whether the kanban view should be grouped if no grouping is specified via whether the kanban view should be grouped if no grouping is specified via
the action or the current research. Should be the name of the field to group the action or the current search. Should be the name of the field to group
by when no grouping is otherwise specified by when no grouping is otherwise specified
``default_order`` ``default_order``
cards sorting order used if the user has not already sorted the records (via cards sorting order used if the user has not already sorted the records (via
+1 -1
View File
@@ -105,7 +105,7 @@
</group> </group>
<group string="Advanced Properties"> <group string="Advanced Properties">
<field name="serialization_field_id" <field name="serialization_field_id"
domain="[('ttype','=','serialized'), ('model_id', '=', model_id)]" domain="[('ttype','=','serialized'), ('model_id', '=', parent.model)]"
attrs="{'readonly': [('state','=','base')]}"/> attrs="{'readonly': [('state','=','base')]}"/>
<field name="related"/> <field name="related"/>
<field name="depends"/> <field name="depends"/>
+1 -1
View File
@@ -48,7 +48,7 @@ class Currency(models.Model):
self._cr.execute(query, (date, company_id, tuple(self.ids))) self._cr.execute(query, (date, company_id, tuple(self.ids)))
currency_rates = dict(self._cr.fetchall()) currency_rates = dict(self._cr.fetchall())
for currency in self: for currency in self:
currency.rate = currency_rates.get(currency.id, 1.0) currency.rate = currency_rates.get(currency.id) or 1.0
@api.multi @api.multi
@api.depends('rounding') @api.depends('rounding')
@@ -4,6 +4,7 @@ access_test_new_api_bravo,access_test_new_api_bravo,model_test_new_api_bravo,,1,
access_category,test_new_api_category,test_new_api.model_test_new_api_category,,1,1,1,1 access_category,test_new_api_category,test_new_api.model_test_new_api_category,,1,1,1,1
access_discussion,test_new_api_discussion,test_new_api.model_test_new_api_discussion,,1,1,1,1 access_discussion,test_new_api_discussion,test_new_api.model_test_new_api_discussion,,1,1,1,1
access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1,1,1 access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1,1,1
access_emailmessage,test_new_api_emailmessage,test_new_api.model_test_new_api_emailmessage,,1,1,1,1
access_multi,test_new_api_multi,test_new_api.model_test_new_api_multi,,1,1,1,1 access_multi,test_new_api_multi,test_new_api.model_test_new_api_multi,,1,1,1,1
access_multi_line,test_new_api_multi_line,test_new_api.model_test_new_api_multi_line,,1,1,1,1 access_multi_line,test_new_api_multi_line,test_new_api.model_test_new_api_multi_line,,1,1,1,1
access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1 access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_category test_new_api_category test_new_api.model_test_new_api_category 1 1 1 1
5 access_discussion test_new_api_discussion test_new_api.model_test_new_api_discussion 1 1 1 1
6 access_message test_new_api_message test_new_api.model_test_new_api_message 1 1 1 1
7 access_emailmessage test_new_api_emailmessage test_new_api.model_test_new_api_emailmessage 1 1 1 1
8 access_multi test_new_api_multi test_new_api.model_test_new_api_multi 1 1 1 1
9 access_multi_line test_new_api_multi_line test_new_api.model_test_new_api_multi_line 1 1 1 1
10 access_mixed test_new_api_mixed test_new_api.model_test_new_api_mixed 1 1 1 1
+11
View File
@@ -130,6 +130,9 @@ class Discussion(models.Model):
message_concat = fields.Text(string='Message concatenate') message_concat = fields.Text(string='Message concatenate')
important_messages = fields.One2many('test_new_api.message', 'discussion', important_messages = fields.One2many('test_new_api.message', 'discussion',
domain=[('important', '=', True)]) domain=[('important', '=', True)])
emails = fields.One2many('test_new_api.emailmessage', 'discussion')
important_emails = fields.One2many('test_new_api.emailmessage', 'discussion',
domain=[('important', '=', True)])
@api.onchange('moderator') @api.onchange('moderator')
def _onchange_moderator(self): def _onchange_moderator(self):
@@ -210,6 +213,14 @@ class Message(models.Model):
return [('author.partner_id', operator, value)] return [('author.partner_id', operator, value)]
class EmailMessage(models.Model):
_name = 'test_new_api.emailmessage'
_inherits = {'test_new_api.message': 'message'}
message = fields.Many2one('test_new_api.message', 'Message',
required=True, ondelete='cascade')
email_to = fields.Char('To')
class Multi(models.Model): class Multi(models.Model):
""" Model for testing multiple onchange methods in cascade that modify a """ Model for testing multiple onchange methods in cascade that modify a
one2many field several times. one2many field several times.
@@ -12,6 +12,7 @@ class TestOnChange(common.TransactionCase):
super(TestOnChange, self).setUp() super(TestOnChange, self).setUp()
self.Discussion = self.env['test_new_api.discussion'] self.Discussion = self.env['test_new_api.discussion']
self.Message = self.env['test_new_api.message'] self.Message = self.env['test_new_api.message']
self.EmailMessage = self.env['test_new_api.emailmessage']
def test_default_get(self): def test_default_get(self):
""" checking values returned by default_get() """ """ checking values returned by default_get() """
@@ -88,7 +89,7 @@ class TestOnChange(common.TransactionCase):
self.assertEqual(field_onchange.get('messages'), '1') self.assertEqual(field_onchange.get('messages'), '1')
self.assertItemsEqual( self.assertItemsEqual(
strip_prefix('messages.', field_onchange), strip_prefix('messages.', field_onchange),
['author', 'body', 'name', 'size'], ['author', 'body', 'name', 'size', 'important'],
) )
# modify discussion name # modify discussion name
@@ -104,6 +105,7 @@ class TestOnChange(common.TransactionCase):
'body': BODY, 'body': BODY,
'author': USER.id, 'author': USER.id,
'size': len(BODY), 'size': len(BODY),
'important': False,
}), }),
], ],
} }
@@ -117,12 +119,14 @@ class TestOnChange(common.TransactionCase):
'body': message.body, 'body': message.body,
'author': message.author.name_get()[0], 'author': message.author.name_get()[0],
'size': message.size, 'size': message.size,
'important': message.important,
}), }),
(0, 0, { (0, 0, {
'name': "[%s] %s" % ("Foo", USER.name), 'name': "[%s] %s" % ("Foo", USER.name),
'body': BODY, 'body': BODY,
'author': USER.name_get()[0], 'author': USER.name_get()[0],
'size': len(BODY), 'size': len(BODY),
'important': False,
}), }),
]) ])
@@ -223,3 +227,71 @@ class TestOnChange(common.TransactionCase):
self.assertIn('message_concat', result['value']) self.assertIn('message_concat', result['value'])
self.assertEqual(result['value']['message_concat'], self.assertEqual(result['value']['message_concat'],
"\n".join(["%s:%s" % (m.name, m.body) for m in discussion.messages])) "\n".join(["%s:%s" % (m.name, m.body) for m in discussion.messages]))
def test_onchange_one2many_with_domain_on_related_field(self):
""" test the value of the one2many field when defined with a domain on a related field"""
discussion = self.env.ref('test_new_api.discussion_0')
demo = self.env.ref('base.user_demo')
# mimic UI behaviour, so we get subfields
# (we need at least subfield: 'important_emails.important')
view_info = self.Discussion.fields_view_get(
view_id=self.env.ref('test_new_api.discussion_form').id,
view_type='form')
field_onchange = self.Discussion._onchange_spec(view_info=view_info)
self.assertEqual(field_onchange.get('messages'), '1')
BODY = "What a beautiful day!"
USER = self.env.user
# create standalone email
email = self.EmailMessage.create({
'discussion': discussion.id,
'name': "[%s] %s" % ('', USER.name),
'body': BODY,
'author': USER.id,
'important': False,
'email_to': demo.email,
})
# check if server-side cache is working correctly
self.env.invalidate_all()
self.assertIn(email, discussion.emails)
self.assertNotIn(email, discussion.important_emails)
email.important = True
self.assertIn(email, discussion.important_emails)
# check that when trigger an onchange, we don't reset important emails
# (force `invalidate_all` as but appear in onchange only when we get a
# cache miss)
self.env.invalidate_all()
self.assertEqual(len(discussion.messages), 4)
values = {
'name': "Foo Bar",
'moderator': demo.id,
'categories': [(4, cat.id) for cat in discussion.categories],
'messages': [(4, msg.id) for msg in discussion.messages],
'participants': [(4, usr.id) for usr in discussion.participants],
'message_changes': 0,
'important_messages': [(4, msg.id) for msg in discussion.important_messages],
'important_emails': [(4, eml.id) for eml in discussion.important_emails],
}
result = discussion.onchange(values, 'name', field_onchange)
# When one2many domain contains non-computed field, things are ok
self.assertEqual(result['value']['important_messages'],
[(5,)] + [(4, msg.id) for msg in discussion.important_messages])
# But here with commit 5676d81, we get value of: [(2, email.id)]
self.assertEqual(
result['value']['important_emails'],
[(5,),
(1, email.id, {
'name': u'[Foo Bar] %s' % USER.name,
'body': email.body,
'author': USER.name_get()[0],
'important': True,
'email_to': demo.email,
'size': email.size,
})]
)
+23
View File
@@ -59,12 +59,35 @@
<tree string="Messages"> <tree string="Messages">
<field name="name"/> <field name="name"/>
<field name="body"/> <field name="body"/>
<field name="important"/>
</tree> </tree>
<form string="Message" version="7.0"> <form string="Message" version="7.0">
<group> <group>
<field name="name"/> <field name="name"/>
<field name="author"/> <field name="author"/>
<field name="size"/> <field name="size"/>
<field name="important"/>
</group>
<label for="body"/>
<field name="body"/>
</form>
</field>
</page>
<page string="Emails">
<field name="important_emails">
<tree name="Important Messages">
<field name="name"/>
<field name="body"/>
<field name="important"/>
<field name="email_to"/>
</tree>
<form string="Important Message" version="7.0">
<group>
<field name="name"/>
<field name="author"/>
<field name="size"/>
<field name="important"/>
<field name="email_to"/>
</group> </group>
<label for="body"/> <label for="body"/>
<field name="body"/> <field name="body"/>
+7
View File
@@ -1024,6 +1024,13 @@ class Field(object):
# ``records``, except fields currently being computed # ``records``, except fields currently being computed
spec = [] spec = []
for field, path in records._field_triggers[self]: for field, path in records._field_triggers[self]:
if not field.compute:
# Note: do not invalidate non-computed fields. Such fields may
# require invalidation in general (like *2many fields with
# domains) but should not be invalidated in this case, because
# we would simply lose their values during an onchange!
continue
target = env[field.model_name] target = env[field.model_name]
computed = target.browse(env.computed[field]) computed = target.browse(env.computed[field])
if path == 'id' and field.model_name == records._name: if path == 'id' and field.model_name == records._name:
+1 -1
View File
@@ -2720,8 +2720,8 @@ class BaseModel(object):
self._init_column(cr, k, context=context) self._init_column(cr, k, context=context)
# add the NOT NULL constraint # add the NOT NULL constraint
try: try:
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
cr.commit() cr.commit()
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
_schema.debug("Table '%s': column '%s': added NOT NULL constraint", _schema.debug("Table '%s': column '%s': added NOT NULL constraint",
self._table, k) self._table, k)
except Exception: except Exception:
+8 -2
View File
@@ -961,7 +961,10 @@ class expression(object):
if right is not False: if right is not False:
if isinstance(right, basestring): if isinstance(right, basestring):
op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator) op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
ids2 = [x[0] for x in comodel.name_search(cr, uid, right, [], op, context=context, limit=None)] domain = column._domain
if callable(domain):
domain = domain(model)
ids2 = [x[0] for x in comodel.name_search(cr, uid, right, domain or [], op, context=context, limit=None)]
if ids2: if ids2:
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
elif isinstance(right, collections.Iterable): elif isinstance(right, collections.Iterable):
@@ -1012,7 +1015,10 @@ class expression(object):
if right is not False: if right is not False:
if isinstance(right, basestring): if isinstance(right, basestring):
op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator) op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
res_ids = [x[0] for x in comodel.name_search(cr, uid, right, [], op, context=context, limit=None)] domain = column._domain
if callable(domain):
domain = domain(model)
res_ids = [x[0] for x in comodel.name_search(cr, uid, right, domain or [], op, context=context, limit=None)]
if res_ids: if res_ids:
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
else: else:
+5
View File
@@ -102,6 +102,11 @@ def exp_duplicate_database(db_original_name, db_name):
_drop_conn(cr, db_original_name) _drop_conn(cr, db_original_name)
cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (db_name, db_original_name)) cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (db_name, db_original_name))
registry = openerp.modules.registry.RegistryManager.new(db_name)
with registry.cursor() as cr:
# if it's a copy of a database, force generation of a new dbuuid
registry['ir.config_parameter'].init(cr, force=True)
from_fs = openerp.tools.config.filestore(db_original_name) from_fs = openerp.tools.config.filestore(db_original_name)
to_fs = openerp.tools.config.filestore(db_name) to_fs = openerp.tools.config.filestore(db_name)
if os.path.exists(from_fs) and not os.path.exists(to_fs): if os.path.exists(from_fs) and not os.path.exists(to_fs):
+1 -1
View File
@@ -77,7 +77,7 @@ class configmanager(object):
# Not exposed in the configuration file. # Not exposed in the configuration file.
self.blacklist_for_save = set([ self.blacklist_for_save = set([
'publisher_warranty_url', 'load_language', 'root_path', 'publisher_warranty_url', 'load_language', 'root_path',
'init', 'save', 'config', 'update', 'stop_after_init' 'init', 'save', 'config', 'update', 'stop_after_init', 'dev_mode'
]) ])
# dictionary mapping option destination (keys in self.options) to MyOptions. # dictionary mapping option destination (keys in self.options) to MyOptions.
+9
View File
@@ -510,6 +510,15 @@ def mod10r(number):
report = codec[ (int(digit) + report) % 10 ] report = codec[ (int(digit) + report) % 10 ]
return result + str((10 - report) % 10) return result + str((10 - report) % 10)
def str2bool(s, default=None):
s = ustr(s).lower()
y = 'y yes 1 true t on'.split()
n = 'n no 0 false f off'.split()
if s not in (y + n):
if default is None:
raise ValueError('Use 0/1/yes/no/true/false/on/off')
return bool(default)
return s in y
def human_size(sz): def human_size(sz):
""" """