[MERGE] forward port of branch saas-10 up to d73f0d8
This commit is contained in:
@@ -422,20 +422,28 @@ class AccountBankStatementLine(models.Model):
|
||||
def button_cancel_reconciliation(self):
|
||||
moves_to_unbind = self.env['account.move']
|
||||
moves_to_cancel = self.env['account.move']
|
||||
payment_to_unreconcile = self.env['account.payment']
|
||||
for st_line in self:
|
||||
moves_to_unbind |= st_line.journal_entry_ids
|
||||
for move in st_line.journal_entry_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
|
||||
moves_to_cancel |= st_line.journal_entry_ids
|
||||
if moves_to_unbind:
|
||||
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:
|
||||
for move in moves_to_cancel:
|
||||
move.line_ids.remove_move_reconcile()
|
||||
moves_to_cancel.button_cancel()
|
||||
moves_to_cancel.unlink()
|
||||
|
||||
if payment_to_unreconcile:
|
||||
payment_to_unreconcile.write({'state': 'posted'})
|
||||
|
||||
####################################################
|
||||
# Reconciliation interface methods
|
||||
####################################################
|
||||
@@ -567,7 +575,18 @@ class AccountBankStatementLine(models.Model):
|
||||
else:
|
||||
domain = [(f, '>', 0), (f, '<', amount)]
|
||||
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:
|
||||
raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'"))
|
||||
domain += [('currency_id', '=', c)]
|
||||
|
||||
@@ -717,6 +717,9 @@ class AccountInvoice(models.Model):
|
||||
line2[tmp]['credit'] = (am < 0) and -am or 0.0
|
||||
line2[tmp]['amount_currency'] += l['amount_currency']
|
||||
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:
|
||||
line2[tmp] = l
|
||||
line = []
|
||||
|
||||
@@ -142,11 +142,22 @@ class account_journal(models.Model):
|
||||
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_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')])
|
||||
for ac_bnk in ac_bnk_stmt:
|
||||
for line in ac_bnk.line_ids:
|
||||
if not line.journal_entry_ids:
|
||||
number_to_reconcile += 1
|
||||
#Get the number of items to reconcile for that bank journal
|
||||
self.env.cr.execute("""SELECT COUNT(DISTINCT(statement_line_id))
|
||||
FROM account_move where statement_line_id
|
||||
IN (SELECT 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),))
|
||||
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
|
||||
account_ids = tuple(filter(None, [self.default_debit_account_id.id, self.default_credit_account_id.id]))
|
||||
if account_ids:
|
||||
|
||||
@@ -583,7 +583,14 @@ class AccountMoveLine(models.Model):
|
||||
]
|
||||
try:
|
||||
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])
|
||||
except:
|
||||
pass
|
||||
@@ -1067,13 +1074,13 @@ class AccountMoveLine(models.Model):
|
||||
if vals['debit'] != 0.0: vals['debit'] = res['total_excluded']
|
||||
if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded']
|
||||
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
|
||||
for tax_vals in res['taxes']:
|
||||
if tax_vals['amount']:
|
||||
account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id'])
|
||||
if not account_id: account_id = vals['account_id']
|
||||
tax_lines_vals.append({
|
||||
temp = {
|
||||
'account_id': account_id,
|
||||
'name': vals['name'] + ' ' + tax_vals['name'],
|
||||
'tax_line_id': tax_vals['id'],
|
||||
@@ -1082,7 +1089,15 @@ class AccountMoveLine(models.Model):
|
||||
'statement_id': vals.get('statement_id'),
|
||||
'debit': 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)
|
||||
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', '')
|
||||
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
|
||||
if 'statement_id' in vals and self.payment_id:
|
||||
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
|
||||
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')):
|
||||
self.payment_id.state = 'reconciled'
|
||||
for record in self:
|
||||
if 'statement_id' in vals and record.payment_id:
|
||||
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
|
||||
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)
|
||||
if self._context.get('check_move_validity', True):
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
from openerp import api, models
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
|
||||
class ReportPartnerLedger(models.AbstractModel):
|
||||
@@ -26,7 +28,12 @@ class ReportPartnerLedger(models.AbstractModel):
|
||||
self.env.cr.execute(query, tuple(params))
|
||||
res = self.env.cr.dictfetchall()
|
||||
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:
|
||||
r['date'] = datetime.strptime(r['date'], DEFAULT_SERVER_DATE_FORMAT).strftime(date_format)
|
||||
r['displayed_name'] = '-'.join(
|
||||
r[field_name] for field_name in ('move_name', 'ref', 'name')
|
||||
if r[field_name] not in (None, '', '/')
|
||||
|
||||
@@ -36,7 +36,9 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
|
||||
if (match) {
|
||||
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.
|
||||
if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true")
|
||||
|
||||
@@ -13,8 +13,13 @@ return {
|
||||
init: function() {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.__on_barcode_scanned = function () {
|
||||
self.on_barcode_scanned.apply(self, arguments);
|
||||
this.__on_barcode_scanned = function (barcode, target) {
|
||||
// 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();
|
||||
// Handlers inside a View managed by a ViewManager only listen to barcode events while their view is displayed
|
||||
|
||||
@@ -20,7 +20,7 @@ class crm_stage(osv.Model):
|
||||
_name = "crm.stage"
|
||||
_description = "Stage of case"
|
||||
_rec_name = 'name'
|
||||
_order = "sequence"
|
||||
_order = "sequence, name, id"
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Stage Name', required=True, translate=True),
|
||||
|
||||
@@ -478,7 +478,7 @@ class MassMailing(osv.Model):
|
||||
'create_date': fields.datetime('Creation Date'),
|
||||
'sent_date': fields.datetime('Sent Date', oldname='date', copy=False),
|
||||
'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(
|
||||
'ir.attachment', 'mass_mailing_ir_attachments_rel',
|
||||
'mass_mailing_id', 'attachment_id', 'Attachments'
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
<field name="price_unit" widget="monetary"/>
|
||||
<field name="price_subtotal" 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"/>
|
||||
</group>
|
||||
</form>
|
||||
|
||||
@@ -716,8 +716,9 @@ class product_template(osv.osv):
|
||||
def copy(self, cr, uid, id, default=None, context=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
template = self.browse(cr, uid, id, context=context)
|
||||
default['name'] = _("%s (copy)") % (template['name'])
|
||||
if 'name' not in default:
|
||||
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)
|
||||
|
||||
_defaults = {
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ class SaleOrder(models.Model):
|
||||
for order in self:
|
||||
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.")
|
||||
client_order_ref = fields.Char(string='Customer Reference', copy=False)
|
||||
|
||||
|
||||
@@ -214,6 +214,7 @@ class PushedFlow(models.Model):
|
||||
'propagate': self.propagate,
|
||||
'push_rule_id': self.id,
|
||||
'warehouse_id': self.warehouse_id.id,
|
||||
'procurement_id': False,
|
||||
})
|
||||
move.write({'move_dest_id': new_move.id})
|
||||
new_move.action_confirm()
|
||||
|
||||
@@ -37,6 +37,7 @@ from openerp.modules import get_resource_path
|
||||
from openerp.tools import topological_sort
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools import ustr
|
||||
from openerp.tools.misc import str2bool
|
||||
from openerp import http
|
||||
from openerp.http import request, serialize_exception as _serialize_exception
|
||||
from openerp.exceptions import AccessError
|
||||
@@ -695,7 +696,7 @@ class Database(http.Controller):
|
||||
def restore(self, master_pwd, backup_file, name, copy=False):
|
||||
try:
|
||||
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')
|
||||
except Exception, e:
|
||||
error = "Database restore error: %s" % e
|
||||
|
||||
@@ -241,7 +241,7 @@ with directly by external objects or search view controls
|
||||
.. js:class:: openerp.web.search.Facet
|
||||
|
||||
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).
|
||||
|
||||
.. js:attribute:: category
|
||||
|
||||
@@ -66,10 +66,45 @@ Tour.register({
|
||||
},
|
||||
{
|
||||
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
|
||||
{
|
||||
title: "Select First Tab",
|
||||
element: 'a[role=tab]:first',
|
||||
},
|
||||
{
|
||||
title: "create new message 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",
|
||||
element: '.tab-pane:eq(1).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"]'
|
||||
element: '.tab-pane:eq(2).active .o_form_field_x2many_list_row_add a'
|
||||
},
|
||||
{
|
||||
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"]'
|
||||
},
|
||||
{
|
||||
@@ -187,8 +217,8 @@ Tour.register({
|
||||
// save
|
||||
{
|
||||
title: "save discussion",
|
||||
waitFor: '.tab-pane:eq(1) .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)',
|
||||
waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)',
|
||||
waitNot: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(3)',
|
||||
element: 'button.o_form_button_save'
|
||||
},
|
||||
|
||||
@@ -204,8 +234,8 @@ Tour.register({
|
||||
},
|
||||
{
|
||||
title: "check data 3",
|
||||
waitFor: '.tab-pane:eq(1) .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)',
|
||||
waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)',
|
||||
waitNot: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(3)',
|
||||
},
|
||||
|
||||
// edit
|
||||
@@ -214,7 +244,7 @@ Tour.register({
|
||||
element: 'button.o_form_button_edit'
|
||||
},
|
||||
{
|
||||
title: "change tab to Participants",
|
||||
title: "change tab to Messages",
|
||||
waitFor: '.o_form_editable',
|
||||
element: '[data-toggle="tab"]:contains(Messages)'
|
||||
},
|
||||
@@ -344,8 +374,8 @@ Tour.register({
|
||||
},
|
||||
{
|
||||
title: "check data 7",
|
||||
waitFor: '.tab-pane:eq(1) .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)',
|
||||
waitFor: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[data-id]:eq(2)',
|
||||
waitNot: '.tab-pane:eq(2) .o_form_field.o_view_manager_content tbody tr[date-id]:eq(3)',
|
||||
},
|
||||
|
||||
// edit
|
||||
|
||||
@@ -209,10 +209,19 @@
|
||||
<input id="name" type="text" name="name" class="form-control" required="required"/>
|
||||
</div>
|
||||
<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>
|
||||
<input name="copy" type="checkbox" class="pull-right" value="1">
|
||||
<span>Generate a new database uuid.</span>
|
||||
<input name="copy" type="radio" class="pull-right" value="true" checked="1">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 9.0c\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-01-28 12:13+0000\n"
|
||||
"PO-Revision-Date: 2016-01-28 12:13+0000\n"
|
||||
"POT-Creation-Date: 2016-06-17 08:01+0000\n"
|
||||
"PO-Revision-Date: 2016-06-17 08:01+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -25,14 +25,21 @@ msgstr ""
|
||||
|
||||
#. module: web_settings_dashboard
|
||||
#. 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
|
||||
msgid "Active User"
|
||||
msgstr ""
|
||||
|
||||
#. module: web_settings_dashboard
|
||||
#. 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
|
||||
msgid "App store"
|
||||
msgstr ""
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
<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-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 <= 1"> Active User</t>
|
||||
<t t-if="active_users > 1">Active Users</t>
|
||||
</t>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
<t t-foreach="channels" t-as="channel">
|
||||
<div class="media">
|
||||
<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>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading"><t t-esc="channel.name"/></h4>
|
||||
|
||||
@@ -60,6 +60,7 @@ class website_account(http.Controller):
|
||||
error_message = []
|
||||
|
||||
mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id"]
|
||||
optional_billing_fields = ["zipcode", "state_id", "vat", "street"]
|
||||
|
||||
# Validation
|
||||
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']:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -680,7 +680,7 @@ Model Reference
|
||||
.. automethod:: read
|
||||
.. automethod:: read_group
|
||||
|
||||
.. rubric:: Research
|
||||
.. rubric:: Searching
|
||||
|
||||
.. automethod:: search
|
||||
.. automethod:: search_count
|
||||
|
||||
@@ -822,7 +822,7 @@ attributes:
|
||||
|
||||
``default_group_by``
|
||||
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
|
||||
``default_order``
|
||||
cards sorting order used if the user has not already sorted the records (via
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
</group>
|
||||
<group string="Advanced Properties">
|
||||
<field name="serialization_field_id"
|
||||
domain="[('ttype','=','serialized'), ('model_id', '=', model_id)]"
|
||||
domain="[('ttype','=','serialized'), ('model_id', '=', parent.model)]"
|
||||
attrs="{'readonly': [('state','=','base')]}"/>
|
||||
<field name="related"/>
|
||||
<field name="depends"/>
|
||||
|
||||
@@ -48,7 +48,7 @@ class Currency(models.Model):
|
||||
self._cr.execute(query, (date, company_id, tuple(self.ids)))
|
||||
currency_rates = dict(self._cr.fetchall())
|
||||
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.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_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_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_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
|
||||
|
||||
|
@@ -130,6 +130,9 @@ class Discussion(models.Model):
|
||||
message_concat = fields.Text(string='Message concatenate')
|
||||
important_messages = fields.One2many('test_new_api.message', 'discussion',
|
||||
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')
|
||||
def _onchange_moderator(self):
|
||||
@@ -210,6 +213,14 @@ class Message(models.Model):
|
||||
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):
|
||||
""" Model for testing multiple onchange methods in cascade that modify a
|
||||
one2many field several times.
|
||||
|
||||
@@ -12,6 +12,7 @@ class TestOnChange(common.TransactionCase):
|
||||
super(TestOnChange, self).setUp()
|
||||
self.Discussion = self.env['test_new_api.discussion']
|
||||
self.Message = self.env['test_new_api.message']
|
||||
self.EmailMessage = self.env['test_new_api.emailmessage']
|
||||
|
||||
def test_default_get(self):
|
||||
""" checking values returned by default_get() """
|
||||
@@ -88,7 +89,7 @@ class TestOnChange(common.TransactionCase):
|
||||
self.assertEqual(field_onchange.get('messages'), '1')
|
||||
self.assertItemsEqual(
|
||||
strip_prefix('messages.', field_onchange),
|
||||
['author', 'body', 'name', 'size'],
|
||||
['author', 'body', 'name', 'size', 'important'],
|
||||
)
|
||||
|
||||
# modify discussion name
|
||||
@@ -104,6 +105,7 @@ class TestOnChange(common.TransactionCase):
|
||||
'body': BODY,
|
||||
'author': USER.id,
|
||||
'size': len(BODY),
|
||||
'important': False,
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -117,12 +119,14 @@ class TestOnChange(common.TransactionCase):
|
||||
'body': message.body,
|
||||
'author': message.author.name_get()[0],
|
||||
'size': message.size,
|
||||
'important': message.important,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': "[%s] %s" % ("Foo", USER.name),
|
||||
'body': BODY,
|
||||
'author': USER.name_get()[0],
|
||||
'size': len(BODY),
|
||||
'important': False,
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -223,3 +227,71 @@ class TestOnChange(common.TransactionCase):
|
||||
self.assertIn('message_concat', result['value'])
|
||||
self.assertEqual(result['value']['message_concat'],
|
||||
"\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,
|
||||
})]
|
||||
)
|
||||
|
||||
@@ -59,12 +59,35 @@
|
||||
<tree string="Messages">
|
||||
<field name="name"/>
|
||||
<field name="body"/>
|
||||
<field name="important"/>
|
||||
</tree>
|
||||
<form string="Message" version="7.0">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="author"/>
|
||||
<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>
|
||||
<label for="body"/>
|
||||
<field name="body"/>
|
||||
|
||||
@@ -1024,6 +1024,13 @@ class Field(object):
|
||||
# ``records``, except fields currently being computed
|
||||
spec = []
|
||||
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]
|
||||
computed = target.browse(env.computed[field])
|
||||
if path == 'id' and field.model_name == records._name:
|
||||
|
||||
+1
-1
@@ -2720,8 +2720,8 @@ class BaseModel(object):
|
||||
self._init_column(cr, k, context=context)
|
||||
# add the NOT NULL constraint
|
||||
try:
|
||||
cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
|
||||
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",
|
||||
self._table, k)
|
||||
except Exception:
|
||||
|
||||
@@ -961,7 +961,10 @@ class expression(object):
|
||||
if right is not False:
|
||||
if isinstance(right, basestring):
|
||||
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:
|
||||
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
|
||||
elif isinstance(right, collections.Iterable):
|
||||
@@ -1012,7 +1015,10 @@ class expression(object):
|
||||
if right is not False:
|
||||
if isinstance(right, basestring):
|
||||
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:
|
||||
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
|
||||
else:
|
||||
|
||||
@@ -102,6 +102,11 @@ def exp_duplicate_database(db_original_name, db_name):
|
||||
_drop_conn(cr, 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)
|
||||
to_fs = openerp.tools.config.filestore(db_name)
|
||||
if os.path.exists(from_fs) and not os.path.exists(to_fs):
|
||||
|
||||
@@ -77,7 +77,7 @@ class configmanager(object):
|
||||
# Not exposed in the configuration file.
|
||||
self.blacklist_for_save = set([
|
||||
'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.
|
||||
|
||||
@@ -510,6 +510,15 @@ def mod10r(number):
|
||||
report = codec[ (int(digit) + 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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user