[MERGE] forward port branch saas-14 up to 284ed9216c

This commit is contained in:
Christophe Simonis
2018-06-12 12:19:14 +02:00
32 changed files with 154 additions and 30 deletions
+9 -2
View File
@@ -1270,6 +1270,13 @@ class AccountInvoiceLine(models.Model):
return accounts['income']
return accounts['expense']
def _set_currency(self):
company = self.invoice_id.company_id
currency = self.invoice_id.currency_id
if company and currency:
if company.currency_id != currency:
self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
def _set_taxes(self):
""" Used in on_change to set taxes and price."""
if self.invoice_id.type in ('out_invoice', 'out_refund'):
@@ -1288,8 +1295,10 @@ class AccountInvoiceLine(models.Model):
prec = self.env['decimal.precision'].precision_get('Product Price')
if not self.price_unit or float_compare(self.price_unit, self.product_id.standard_price, precision_digits=prec) == 0:
self.price_unit = fix_price(self.product_id.standard_price, taxes, fp_taxes)
self._set_currency()
else:
self.price_unit = fix_price(self.product_id.lst_price, taxes, fp_taxes)
self._set_currency()
@api.onchange('product_id')
def _onchange_product_id(self):
@@ -1338,8 +1347,6 @@ class AccountInvoiceLine(models.Model):
domain['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)]
if company and currency:
if company.currency_id != currency:
self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.invoice_id.date_invoice)).rate
if self.uom_id and self.uom_id.id != product.uom_id.id:
self.price_unit = product.uom_id._compute_price(self.price_unit, self.uom_id)
+1 -1
View File
@@ -18,7 +18,7 @@ class AccountConfigSettings(models.TransientModel):
'account.journal',
related='company_id.currency_exchange_journal_id',
string="Exchange Gain or Loss Journal",
domain=[('type', '=', 'general')],
domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
help='The accounting journal where automatic exchange differences will be registered')
has_chart_of_accounts = fields.Boolean(compute='_compute_has_chart_of_accounts', string='Company has a chart of accounts')
chart_template_id = fields.Many2one('account.chart.template', string='Template',
@@ -240,14 +240,16 @@
<span><t t-esc="dashboard.account_balance"/></span>
</div>
</div>
<div class="row" name="latest_statement" t-if="dashboard.last_balance != dashboard.account_balance">
<div class="col-xs-6">
<span title="Latest Statement">Latest Statement</span>
<t t-if="dashboard.last_balance != dashboard.account_balance">
<div class="row" name="latest_statement">
<div class="col-xs-6">
<span title="Latest Statement">Latest Statement</span>
</div>
<div class="col-xs-6 text-right">
<span><t t-esc="dashboard.last_balance"/></span>
</div>
</div>
<div class="col-xs-6 text-right">
<span><t t-esc="dashboard.last_balance"/></span>
</div>
</div>
</t>
</div>
</t>
<t t-name="JournalBodySalePurchase">
@@ -50,7 +50,11 @@ class AccountBankStatementImport(models.TransientModel):
# Create the bank statements
statement_ids, notifications = self._create_bank_statements(stmts_vals)
# Now that the import worked out, set it as the bank_statements_source of the journal
journal.bank_statements_source = 'file_import'
if journal.bank_statements_source != 'file_import':
# Use sudo() because only 'account.group_account_manager'
# has write access on 'account.journal', but 'account.group_account_user'
# must be able to import bank statement files
journal.sudo().bank_statements_source = 'file_import'
# Finally dispatch to reconciliation interface
action = self.env.ref('account.action_bank_reconcile_bank_statements')
return {
+3 -2
View File
@@ -260,7 +260,7 @@ class MergePartnerAutomatic(models.TransientModel):
"""
_logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)
model_fields = dst_partner._fields
model_fields = dst_partner.fields_get().keys()
def write_serializer(item):
if isinstance(item, models.BaseModel):
@@ -269,7 +269,8 @@ class MergePartnerAutomatic(models.TransientModel):
return item
# get all fields that are not computed or x2many
values = dict()
for column, field in model_fields.iteritems():
for column in model_fields:
field = dst_partner._fields[column]
if field.type not in ('many2many', 'one2many') and field.compute is None:
for item in itertools.chain(src_partners, [dst_partner]):
if item[column]:
+1 -1
View File
@@ -10,7 +10,7 @@ class ResPartner(models.Model):
def _compute_event_count(self):
for partner in self:
partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
partner.event_count = self.env['event.event'].sudo().search_count([('registration_ids.partner_id', 'child_of', partner.ids)])
@api.multi
def action_event_view(self):
+2
View File
@@ -81,6 +81,8 @@ class MrpBom(models.Model):
def onchange_product_tmpl_id(self):
if self.product_tmpl_id:
self.product_uom_id = self.product_tmpl_id.uom_id.id
if self.product_id.product_tmpl_id != self.product_tmpl_id:
self.product_id = False
@api.onchange('routing_id')
def onchange_routing_id(self):
+1
View File
@@ -318,6 +318,7 @@ class MrpWorkorder(models.Model):
move_lot = production_move.move_lot_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
if move_lot:
move_lot.quantity += self.qty_producing
move_lot.quantity_done += self.qty_producing
else:
move_lot.create({'move_id': production_move.id,
'lot_id': self.final_lot_id.id,
+1 -1
View File
@@ -191,7 +191,7 @@
<label for="duration"/>
<div>
<button style="pointer-events: none;" class="oe_inline label label-default">
<field name="duration" widget="mrp_time_counter"/>
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/>
</button>
</div>
</group>
@@ -79,6 +79,7 @@ odoo.define('payment_stripe.stripe', function(require) {
})
handler.open({
name: $("input[name='merchant']").val(),
email: $("input[name='email']").val(),
description: $("input[name='invoice_num']").val(),
currency: currency,
amount: _.contains(int_currencies, currency) ? amount : amount * 100,
@@ -94,6 +95,7 @@ odoo.define('payment_stripe.stripe', function(require) {
$form.html(data);
handler.open({
name: $("input[name='merchant']").val(),
email: $("input[name='email']").val(),
description: $("input[name='invoice_num']").val(),
currency: currency,
amount: _.contains(int_currencies, currency) ? amount : amount * 100,
+2 -2
View File
@@ -38,8 +38,8 @@ class PosSession(models.Model):
paid=order.amount_paid,
))
order.action_pos_order_done()
orders = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'])
orders.sudo()._reconcile_payments()
orders_to_reconcile = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'] and order.partner_id)
orders_to_reconcile.sudo()._reconcile_payments()
config_id = fields.Many2one(
'pos.config', string='Point of Sale',
+1 -1
View File
@@ -140,7 +140,7 @@ class PortalWizardUser(models.TransientModel):
if wizard_user.partner_id.company_id:
company_id = wizard_user.partner_id.company_id.id
else:
company_id = self.env['res.company']._company_default_get('res.users')
company_id = self.env['res.company']._company_default_get('res.users').id
user_portal = wizard_user.sudo().with_context(company_id=company_id)._create_user()
else:
user_portal = user
+2
View File
@@ -245,6 +245,7 @@ class ProductProduct(models.Model):
for supplier_info in self.seller_ids:
if supplier_info.name.id == self._context.get('partner_id'):
self.code = supplier_info.product_code or self.default_code
break
else:
self.code = self.default_code
@@ -253,6 +254,7 @@ class ProductProduct(models.Model):
for supplier_info in self.seller_ids:
if supplier_info.name.id == self._context.get('partner_id'):
product_name = supplier_info.product_name or self.default_code
break
else:
product_name = self.name
self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name)
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_variants, test_uom, test_pricelist, test_product_pricelist
from . import test_seller, test_variants, test_uom, test_pricelist, test_product_pricelist
+28
View File
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestSeller(TransactionCase):
def setUp(self):
super(TestSeller, self).setUp()
self.product_service = self.env.ref('product.product_product_2')
self.product_service.default_code = 'DEFCODE'
self.asustec = self.env.ref('base.res_partner_1')
self.camptocamp = self.env.ref('base.res_partner_12')
def test_10_sellers(self):
self.product_service.write({'seller_ids': [
(0, 0, {'name': self.asustec.id, 'product_code': 'ASUCODE'}),
(0, 0, {'name': self.camptocamp.id, 'product_code': 'C2CCODE'}),
]})
default_code = self.product_service.code
self.assertEqual("DEFCODE", default_code, "Default code not used in product name")
context_code = self.product_service\
.with_context(partner_id=self.camptocamp.id)\
.code
self.assertEqual('C2CCODE', context_code, "Partner's code not used in product name with context set")
@@ -42,12 +42,16 @@ var EventRegistrationForm = Widget.extend({
$button.attr('disabled', true);
return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) {
var $modal = $(modal);
$modal.modal({backdrop: 'static', keyboard: false});
$modal.find('.modal-body > div').removeClass('container'); // retrocompatibility - REMOVE ME in master / saas-19
$modal.insertAfter($form).modal();
$modal.on('click', '.js_goto_event', function () {
$modal.modal('hide');
$button.prop('disabled', false);
});
$modal.on('click', '.close', function () {
$button.prop('disabled', false);
});
});
}
},
+5
View File
@@ -427,6 +427,11 @@ class Post(models.Model):
raise KarmaError('User karma not sufficient to post an image or link.')
return content
@api.constrains('parent_id')
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('You cannot create recursive forum posts.'))
@api.model
def create(self, vals):
if 'content' in vals and vals.get('forum_id'):
+7 -3
View File
@@ -20,13 +20,17 @@ class WebsiteLivechat(http.Controller):
@http.route('/livechat/channel/<model("im_livechat.channel"):channel>', type='http', auth='public', website=True)
def channel_rating(self, channel, **kw):
# get the last 100 ratings and the repartition per grade
ratings = request.env['rating.rating'].search([('res_model', '=', 'mail.channel'), ('res_id', 'in', channel.sudo().channel_ids.ids)], order='create_date desc', limit=100)
repartition = channel.sudo().channel_ids.rating_get_grades()
domain = [
('res_model', '=', 'mail.channel'), ('res_id', 'in', channel.sudo().channel_ids.ids),
('consumed', '=', True), ('rating', '>=', 1),
]
ratings = request.env['rating.rating'].search(domain, order='create_date desc', limit=100)
repartition = channel.sudo().channel_ids.rating_get_grades(domain=domain)
# compute percentage
percentage = dict.fromkeys(['great', 'okay', 'bad'], 0)
for grade in repartition:
percentage[grade] = repartition[grade] * 100 / sum(repartition.values()) if sum(repartition.values()) else 0
percentage[grade] = round(repartition[grade] * 100.0 / sum(repartition.values()), 1) if sum(repartition.values()) else 0
# the value dict to render the template
values = {
@@ -49,7 +49,7 @@ class Issue(models.Model):
groups = super(Issue, self)._notification_recipients(message, groups)
for group_name, group_method, group_data in groups:
if group_name in ["customer", "portal"]:
if group_name == 'customer':
continue
group_data['has_button_access'] = True
+1 -1
View File
@@ -617,7 +617,7 @@
<input type="hidden" t-if="len(product.product_variant_ids) == 1" class="product_id" name="product_id" t-att-value="product.product_variant_id.id"/>
<t t-if="len(product.product_variant_ids) &gt; 1">
<label label-default="label-default" class="radio" t-foreach="product.product_variant_ids" t-as="variant_id">
<input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else ''" t-att-value="variant_id.id" t-att-data-lst_price="compute_currency(variant_id.lst_price)" t-att-data-price="variant_id.price"/>
<input type="radio" name="product_id" class="js_product_change" t-att-checked="'checked' if variant_id_index == 0 else None" t-att-value="variant_id.id" t-att-data-lst_price="compute_currency(variant_id.lst_price)" t-att-data-price="variant_id.price"/>
<span t-esc="variant_id.name_get()[0][1]"/>
<t t-set="diff_price" t-value="variant_id.price - variant_id.product_tmpl_id.price"/>
<span class="badge" t-if="diff_price != 0">
+17
View File
@@ -0,0 +1,17 @@
China, 2018-04-28
Shine IT 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,
Tony Gu tony@openerp.cn https://github.com/digitalsatori
List of contributors:
Tony Gu tony@openerp.cn https://github.com/digitalsatori
Joshua Jan joshua@openerp.cn https://github.com/joshuajan
Xiao Guo xerxes@openerp.cn https://github.com/xerxesnoPT
+1
View File
@@ -13,3 +13,4 @@ Wolfgang Taferner wolfgang.taferner@wt-io-it.at https://github.com/wt-io-it
List of contributors:
Wolfgang Taferner wolfgang.taferner@wt-io-it.at https://github.com/wtaferner
Andreas Perhab andreas.perhab@wt-io-it.at https://github.com/ap-wtioit
+11
View File
@@ -0,0 +1,11 @@
France, 2018-06-07
I here by 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,
Flavien CHÊNE me@flavienchene.fr https://github.com/Zelwak
+11
View File
@@ -0,0 +1,11 @@
France, 2018-05-24
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,
Martin Duvergey martin.duvergey@gpsante.fr https://github.com/mduvergey
+11
View File
@@ -0,0 +1,11 @@
Belgium, 2018-05-17
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,
Rod Schouteden rod@schout-it.be https://github.com/schout-it
+1 -1
View File
@@ -45,7 +45,7 @@ On Linux, using an installer
.. code-block:: console
$ python /usr/bin/odoo.py -d <database_name> -i web_enterprise --stop-after-init
$ python /usr/bin/odoo-bin -d <database_name> -i web_enterprise --stop-after-init
* You should be able to connect to your Odoo Enterprise instance using your usual mean of identification.
You can then link your database with your Odoo Enterprise Subscription by entering the code you received
+4 -2
View File
@@ -20,6 +20,8 @@ from odoo.tools import html2text, ustr
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
SMTP_TIMEOUT = 60
class MailDeliveryException(except_orm):
"""Specific exception subclass for mail delivery errors"""
@@ -240,9 +242,9 @@ class IrMailServer(models.Model):
"You could use STARTTLS instead."
"If SSL is needed, an upgrade to Python 2.6 on the server-side "
"should do the trick."))
connection = smtplib.SMTP_SSL(smtp_server, smtp_port)
connection = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
else:
connection = smtplib.SMTP(smtp_server, smtp_port)
connection = smtplib.SMTP(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
connection.set_debuglevel(smtp_debug)
if smtp_encryption == 'starttls':
# starttls() will perform ehlo() if needed first
+5
View File
@@ -780,6 +780,11 @@ class GroupsView(models.Model):
xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace")
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
if not view.check_access_rights('write', raise_exception=False):
# erp manager has the rights to update groups/users but not
# to modify ir.ui.view
if self.env.user.has_group('base.group_erp_manager'):
view = view.sudo()
view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False})
def get_application_groups(self, domain):
+1 -1
View File
@@ -142,7 +142,7 @@
<field name="name">res.partner.rule.private.employee</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="domain_force">
[('type', '!=', 'private')]
['|', ('type', '!=', 'private'), ('type', '=', False)]
</field>
<field name="groups" eval="[
(4, ref('base.group_user')),
+1 -1
View File
@@ -3864,7 +3864,7 @@ class BaseModel(object):
vals = self.copy_data(default)[0]
# To avoid to create a translation in the lang of the user, copy_translation will do it
new = self.with_context(lang=None).create(vals)
self.copy_translations(new)
self.with_context(from_copy_translation=True).copy_translations(new)
return new
@api.multi
+4 -1
View File
@@ -357,7 +357,10 @@ class GeventServer(CommonServer):
def start(self):
import gevent
from gevent.wsgi import WSGIServer
try:
from gevent.pywsgi import WSGIServer
except ImportError:
from gevent.wsgi import WSGIServer
if os.name == 'posix':
+1
View File
@@ -70,6 +70,7 @@ def py2exe_options():
'dist_dir': 'dist',
'packages': [
'asynchat', 'asyncore',
'BeautifulSoup',
'commands',
'dateutil',
'decimal',