[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):
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)]
+3
View File
@@ -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:
+24 -8
View File
@@ -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
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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>
+3 -2
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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()
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
+44 -14
View File
@@ -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
+12 -3
View File
@@ -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 &lt;= 1"> Active User</t>
<t t-if="active_users &gt; 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
+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_group
.. rubric:: Research
.. rubric:: Searching
.. automethod:: search
.. automethod:: search_count
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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
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')
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,
})]
)
+23
View File
@@ -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"/>
+7
View File
@@ -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
View File
@@ -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:
+8 -2
View File
@@ -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:
+5
View File
@@ -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):
+1 -1
View File
@@ -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.
+9
View File
@@ -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):
"""