[MERGE] forward port branch saas-11.3 up to af35aea6b0

This commit is contained in:
Christophe Simonis
2018-08-09 19:33:32 +02:00
50 changed files with 296 additions and 14468 deletions
+2 -1
View File
@@ -1235,8 +1235,9 @@ class AccountMoveLine(models.Model):
an analytic account. This method is intended to be extended in other modules.
"""
amount = (self.credit or 0.0) - (self.debit or 0.0)
default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/'))
return {
'name': self.name,
'name': default_name,
'date': self.date,
'account_id': self.analytic_account_id.id,
'tag_ids': [(6, 0, self._get_analytic_tag_ids())],
@@ -872,6 +872,7 @@ var StatementModel = BasicModel.extend({
}
});
total = Math.round(total*1000)/1000 || 0;
amount_currency = Math.round(amount_currency);
line.balance = {
amount: total,
amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),
@@ -309,8 +309,12 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
self.fields = {
partner_id : new relational_fields.FieldMany2One(self,
'partner_id',
self.model.get(recordID),
{mode: 'edit'}
self.model.get(recordID), {
mode: 'edit',
attrs: {
placeholder: self._initialState.st_line.communication_partner_name || '',
}
}
)
};
self.fields.partner_id.appendTo(self.$('.accounting_view caption'));
@@ -60,6 +60,7 @@ class AssetAssetReport(models.Model):
from account_asset_depreciation_line dl
left join account_asset_asset a on (dl.asset_id=a.id)
left join (select min(d.id) as id,ac.id as ac_id from account_asset_depreciation_line as d inner join account_asset_asset as ac ON (ac.id=d.asset_id) group by ac_id) as dlmin on dlmin.ac_id=a.id
where a.active is true
group by
dl.amount,dl.asset_id,dl.depreciation_date,dl.name,
a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id,
+8 -5
View File
@@ -25,9 +25,9 @@ def now(**kwargs):
class ResPartner(models.Model):
_inherit = 'res.partner'
signup_token = fields.Char(copy=False)
signup_type = fields.Char(string='Signup Token Type', copy=False)
signup_expiration = fields.Datetime(copy=False)
signup_token = fields.Char(copy=False, groups="base.group_erp_manager")
signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager")
signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager")
signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid')
signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL')
@@ -35,15 +35,17 @@ class ResPartner(models.Model):
@api.depends('signup_token', 'signup_expiration')
def _compute_signup_valid(self):
dt = now()
for partner in self:
for partner in self.sudo():
partner.signup_valid = bool(partner.signup_token) and \
(not partner.signup_expiration or dt <= partner.signup_expiration)
@api.multi
def _compute_signup_url(self):
""" proxy for function field towards actual implementation """
result = self._get_signup_url_for_action()
result = self.sudo()._get_signup_url_for_action()
for partner in self:
if any(u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user):
self.env['res.users'].check_access_rights('write')
partner.signup_url = result.get(partner.id, False)
@api.multi
@@ -106,6 +108,7 @@ class ResPartner(models.Model):
allow_signup = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') == 'b2c'
for partner in self:
if allow_signup and not partner.user_ids:
partner = partner.sudo()
partner.signup_prepare()
res[partner.id]['auth_signup_token'] = partner.signup_token
elif partner.user_ids:
+11 -1
View File
@@ -163,10 +163,20 @@ class ResUsers(models.Model):
template = self.env.ref('auth_signup.reset_password_email')
assert template._name == 'mail.template'
template_values = {
'email_to': '${object.email|safe}',
'email_cc': False,
'auto_delete': True,
'partner_to': False,
'scheduled_date': False,
}
template.write(template_values)
for user in self:
if not user.email:
raise UserError(_("Cannot send email: user %s has no email address.") % user.name)
template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True)
with self.env.cr.savepoint():
template.with_context(lang=user.lang).send_mail(user.id, force_send=True, raise_exception=True)
_logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)
@api.model
@@ -143,6 +143,8 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
e.key === "ArrowUp" || e.key === "ArrowDown" ||
e.key === "Escape" || e.key === "Tab" ||
e.key === "Backspace" || e.key === "Delete" ||
e.key === "Home" || e.key === "End" ||
e.key === "PageUp" || e.key === "PageDown" ||
e.key === "Unidentified" || /F\d\d?/.test(e.key)) {
return true;
} else {
+18
View File
@@ -1495,6 +1495,10 @@ class Meeting(models.Model):
@api.multi
def write(self, values):
# FIXME: neverending recurring events
if 'rrule' in values:
values['rrule'] = self._fix_rrule(values)
# compute duration, only if start and stop are modified
if not 'duration' in values and 'start' in values and 'stop' in values:
values['duration'] = self._get_duration(values['start'], values['stop'])
@@ -1563,6 +1567,10 @@ class Meeting(models.Model):
@api.model
def create(self, values):
# FIXME: neverending recurring events
if 'rrule' in values:
values['rrule'] = self._fix_rrule(values)
if not 'user_id' in values: # Else bug with quick_create when we are filter on an other user
values['user_id'] = self.env.user.id
@@ -1769,3 +1777,13 @@ class Meeting(models.Model):
activity_values['user_id'] = values['user_id']
if activity_values.keys():
self.mapped('activity_ids').write(activity_values)
@api.model
def _fix_rrule(self, values):
rule_str = values.get('rrule')
if rule_str:
rule = rrule.rrulestr(rule_str)
if not rule._until and not rule._count:
rule._count = 100
rule_str = str(rule).split('RRULE:')[-1]
return rule_str
+1 -1
View File
@@ -38,7 +38,7 @@ class TestLead2opportunity2win(TestCrmCases):
crm_case_3.message_post(subject='Test note', body='Détails envoyés par le client sur ​​le FAX pour la qualité')
# I convert mass lead into opportunity customer.
mass = CrmLead2OpportunityPartnerMass.with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({
mass = CrmLead2OpportunityPartnerMass.sudo(self.crm_salemanager.id).with_context({'active_model': 'crm.lead', 'active_ids': [crm_case_13.id, crm_case_2.id], 'active_id': crm_case_13.id}).create({
'user_ids': [(6, 0, self.env.ref('base.user_root').ids)],
'team_id': self.env.ref("sales_team.team_sales_department").id
})
@@ -2,4 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
challenge_officer,"Challenge Officer",gamification.model_gamification_challenge,hr.group_hr_user,1,1,1,1
challenge_line_officer,"Challenge Line Officer",gamification.model_gamification_challenge_line,hr.group_hr_user,1,1,1,1
badge_officer,"Badge Officer",gamification.model_gamification_badge,hr.group_hr_user,1,1,1,1
badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,hr.group_hr_user,1,1,1,1
badge_user_officer,"Badge-user Officer",gamification.model_gamification_badge_user,hr.group_hr_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 challenge_officer Challenge Officer gamification.model_gamification_challenge hr.group_hr_user 1 1 1 1
3 challenge_line_officer Challenge Line Officer gamification.model_gamification_challenge_line hr.group_hr_user 1 1 1 1
4 badge_officer Badge Officer gamification.model_gamification_badge hr.group_hr_user 1 1 1 1
5 badge_user_officer Badge-user Officer gamification.model_gamification_badge_user hr.group_hr_user 1 1 1 1 1
+2 -1
View File
@@ -38,6 +38,7 @@ class LeaveReport(models.Model):
def init(self):
tools.drop_view_if_exists(self._cr, 'hr_leave_report')
self._cr.execute("""
CREATE or REPLACE view hr_leave_report as (
SELECT row_number() over(ORDER BY leaves.employee_id) as id,
@@ -61,7 +62,7 @@ class LeaveReport(models.Model):
FALSE as payslip_status,
'allocation' as type
from hr_leave_allocation as allocation
union select
union all select
request.employee_id as employee_id,
request.name as name,
request.number_of_days as number_of_days,
+1 -1
View File
@@ -674,7 +674,7 @@ class Channel(models.Model):
'is_minimized': minimized
}
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
channel_partners = self.env['mail.channel.partner'].search(domain)
channel_partners = self.env['mail.channel.partner'].search(domain, limit=1)
channel_partners.write(values)
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0])
+3 -3
View File
@@ -253,17 +253,17 @@ class MailTemplate(models.Model):
def unlink_action(self):
for template in self:
if template.ref_ir_act_window:
template.ref_ir_act_window.sudo().unlink()
template.ref_ir_act_window.unlink()
return True
@api.multi
def create_action(self):
ActWindowSudo = self.env['ir.actions.act_window'].sudo()
ActWindow = self.env['ir.actions.act_window']
view = self.env.ref('mail.email_compose_message_wizard_form')
for template in self:
button_name = _('Send Mail (%s)') % template.name
action = ActWindowSudo.create({
action = ActWindow.create({
'name': button_name,
'type': 'ir.actions.act_window',
'res_model': 'mail.compose.message',
+5 -2
View File
@@ -9,7 +9,9 @@
<sheet>
<div class="oe_button_box" name="button_box">
<field name="ref_ir_act_window" invisible="1"/>
<button class="oe_stat_button" name="create_action" type="object"
<button class="oe_stat_button"
groups="base.group_system"
name="create_action" type="object"
attrs="{'invisible':[('ref_ir_act_window','!=',False)]}" icon="fa-plus"
help="Display an option on related documents to open a composition wizard with this template">
<div class="o_field_widget o_stat_info">
@@ -17,7 +19,8 @@
<span class="o_stat_text">Context Action</span>
</div>
</button>
<button name="unlink_action" type="object"
<button name="unlink_action" type="object"
groups="base.group_system"
class="oe_stat_button" icon="fa-minus"
attrs="{'invisible':[('ref_ir_act_window','=',False)]}"
help="Remove the contextual action to use this template on related documents" widget="statinfo">
+1 -1
View File
@@ -9,7 +9,7 @@ class ProjectTask(models.Model):
_inherit = ["project.task", 'pad.common']
description_pad = fields.Char('Pad URL', pad_content_field='description', copy=False)
use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad")
use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad", readonly=True)
@api.model
def create(self, vals):
@@ -105,7 +105,10 @@ odoo.define('payment_stripe.stripe', function(require) {
acquirer_id: acquirer_id,
access_token: access_token,
}).then(function(data) {
try { provider_form[0].innerHTML = data; } catch (e) {};
var $pay_stripe = $('#pay_stripe').detach();
try { provider_form[0].innerHTML = data; } catch (e) {}
// Restore 'Pay Now' button HTML since data might have changed it.
$(provider_form[0]).find('#pay_stripe').replaceWith($pay_stripe);
}).done(function () {
getStripeHandler().open({
name: merchant,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1372,6 +1372,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
reload_partners: function(){
var self = this;
return this.pos.load_new_partners().then(function(){
// partners may have changed in the backend
self.partner_cache = new DomCache();
self.render_list(self.pos.db.get_partners_sorted(1000));
// update the currently assigned client if it has been changed in db.
+5 -2
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import float_is_zero
class PosMakePayment(models.TransientModel):
@@ -30,7 +31,7 @@ class PosMakePayment(models.TransientModel):
session_id = fields.Many2one('pos.session', required=True, default=_default_session)
journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal)
amount = fields.Float(digits=(16, 2), required=True, default=_default_amount)
amount = fields.Float(digits=0, required=True, default=_default_amount)
payment_name = fields.Char(string='Payment Reference')
payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Datetime.now())
@@ -49,11 +50,13 @@ class PosMakePayment(models.TransientModel):
"""
self.ensure_one()
order = self.env['pos.order'].browse(self.env.context.get('active_id', False))
currency = order.pricelist_id.currency_id
amount = order.amount_total - order.amount_paid
data = self.read()[0]
# add_payment expect a journal key
data['journal'] = data['journal_id'][0]
if amount != 0.0:
data['amount'] = currency.round(data['amount']) if currency else data['amount']
if not float_is_zero(amount, precision_rounding=currency.rounding or 0.01):
order.add_payment(data)
if order.test_paid():
order.action_pos_order_paid()
+1 -1
View File
@@ -111,7 +111,7 @@ class PurchaseOrder(models.Model):
def action_view_picking(self):
""" This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately.
"""
action = self.env.ref('stock.action_picking_tree')
action = self.env.ref('stock.action_picking_tree_all')
result = action.read()[0]
# override the context to get rid of the default filtering on operation type
result['context'] = {}
@@ -1,7 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_repair_fee_user,Repair Fee user,model_repair_fee,stock.group_stock_user,1,1,1,1
access_repair_user,Repair user,model_repair_order,stock.group_stock_user,1,1,1,1
access_product_pricelist_user,product.pricelist user,product.model_product_pricelist,stock.group_stock_user,1,1,1,1
access_stock_production_lot_user,stock.production.lot user,stock.model_stock_production_lot,stock.group_stock_user,1,1,1,1
access_repair_line_user,repair.line user,model_repair_line,stock.group_stock_user,1,1,1,1
access_account_tax_user,account.tax,account.model_account_tax,stock.group_stock_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_repair_fee_user Repair Fee user model_repair_fee stock.group_stock_user 1 1 1 1
3 access_repair_user Repair user model_repair_order stock.group_stock_user 1 1 1 1
access_product_pricelist_user product.pricelist user product.model_product_pricelist stock.group_stock_user 1 1 1 1
4 access_stock_production_lot_user stock.production.lot user stock.model_stock_production_lot stock.group_stock_user 1 1 1 1
5 access_repair_line_user repair.line user model_repair_line stock.group_stock_user 1 1 1 1
6 access_account_tax_user account.tax account.model_account_tax stock.group_stock_user 1 1 1 1
+1 -1
View File
@@ -44,7 +44,7 @@ def float_to_time(hours):
if hours == 24.0:
return time.max
fractional, integral = math.modf(hours)
return time(int(integral), int(60 * fractional), 0)
return time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0)
def _boundaries(intervals, opening, closing):
+2 -1
View File
@@ -57,5 +57,6 @@ class ProductTemplate(models.Model):
def _onchange_type(self):
""" Force values to stay consistent with integrity constraints """
if self.type == 'consu':
self.invoice_policy = 'order'
if not self.invoice_policy:
self.invoice_policy = 'order'
self.service_type = 'manual'
+1 -1
View File
@@ -1250,13 +1250,13 @@ class SaleOrderLine(models.Model):
@api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id')
def _onchange_discount(self):
self.discount = 0.0
if not (self.product_id and self.product_uom and
self.order_id.partner_id and self.order_id.pricelist_id and
self.order_id.pricelist_id.discount_policy == 'without_discount' and
self.env.user.has_group('sale.group_discount_per_so_line')):
return
self.discount = 0.0
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id.id,
@@ -362,7 +362,7 @@
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container">
<div id="sales_settings_invoicing_policy"
class="col-xs-12 col-md-6 o_setting_box hidden"
class="col-xs-12 col-md-6 o_setting_box"
title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div class="o_setting_right_pane">
<label for="default_invoice_policy"/>
@@ -416,3 +416,75 @@ class TestSaleStock(TestSale):
# check the delivered quantity
self.assertEqual(so1.order_line.qty_delivered, 3.0)
def test_07_forced_qties(self):
""" Make multiple sale order lines of the same product which isn't available in stock. On
the picking, create new move lines (through the detailed operations view). See that the move
lines are correctly dispatched through the moves.
"""
uom_unit = self.env.ref('uom.product_uom_unit')
uom_dozen = self.env.ref('uom.product_uom_dozen')
item1 = self.products['prod_order']
self.assertEqual(item1.uom_id.id, uom_unit.id)
# sell a dozen
so1 = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [
(0, 0, {
'name': item1.name,
'product_id': item1.id,
'product_uom_qty': 1,
'product_uom': uom_dozen.id,
'price_unit': item1.list_price,
}),
(0, 0, {
'name': item1.name,
'product_id': item1.id,
'product_uom_qty': 1,
'product_uom': uom_dozen.id,
'price_unit': item1.list_price,
}),
(0, 0, {
'name': item1.name,
'product_id': item1.id,
'product_uom_qty': 1,
'product_uom': uom_dozen.id,
'price_unit': item1.list_price,
}),
],
})
so1.action_confirm()
self.assertEqual(len(so1.picking_ids.move_lines), 3)
so1.picking_ids.write({
'move_line_ids': [
(0, 0, {
'product_id': item1.id,
'product_uom_qty': 0,
'qty_done': 1,
'product_uom_id': uom_dozen.id,
'location_id': so1.picking_ids.location_id.id,
'location_dest_id': so1.picking_ids.location_dest_id.id,
}),
(0, 0, {
'product_id': item1.id,
'product_uom_qty': 0,
'qty_done': 1,
'product_uom_id': uom_dozen.id,
'location_id': so1.picking_ids.location_id.id,
'location_dest_id': so1.picking_ids.location_dest_id.id,
}),
(0, 0, {
'product_id': item1.id,
'product_uom_qty': 0,
'qty_done': 1,
'product_uom_id': uom_dozen.id,
'location_id': so1.picking_ids.location_id.id,
'location_dest_id': so1.picking_ids.location_dest_id.id,
}),
],
})
so1.picking_ids.button_validate()
self.assertEqual(so1.picking_ids.state, 'done')
self.assertEqual(so1.order_line.mapped('qty_delivered'), [1, 1, 1])
+2
View File
@@ -83,3 +83,5 @@ class ProductTemplate(models.Model):
if self.type == 'service':
self.invoice_policy = 'order'
self.service_type = 'timesheet'
elif self.type == 'consu' and self.service_policy == 'ordered_timesheet':
self.invoice_policy = 'order'
+2 -1
View File
@@ -329,7 +329,8 @@ class ProcurementGroup(models.Model):
for orderpoint in orderpoints:
key = self._procurement_from_orderpoint_get_grouping_key([orderpoint.id])
location_data[key] = makedefault()
if not location_data.get(key):
location_data[key] = makedefault()
location_data[key]['products'] += orderpoint.product_id
location_data[key]['orderpoints'] += orderpoint
location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id])
+3 -2
View File
@@ -605,7 +605,8 @@ class Picking(models.Model):
for ops in pick.move_line_ids.filtered(lambda x: not x.move_id):
# Search move with this product
moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id)
if moves: #could search move that needs it the most (that has some quantities left)
moves = sorted(moves, key=lambda m: m.quantity_done < m.product_qty, reverse=True)
if moves:
ops.move_id = moves[0].id
else:
new_move = self.env['stock.move'].create({
@@ -793,7 +794,7 @@ class Picking(models.Model):
quantity_done[quant.product_id.id] += quant.qty
for pack in self.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id):
quantity_done.setdefault(pack.product_id.id, 0)
quantity_done[pack.product_id.id] += pack.qty_done
quantity_done[pack.product_id.id] += pack.product_uom_id._compute_quantity(pack.qty_done, pack.product_id.uom_id)
return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done)
@api.multi
+5
View File
@@ -593,6 +593,11 @@ class StockMove(models.Model):
def _get_partner_id_for_valuation_lines(self):
return (self.picking_id.partner_id and self.env['res.partner']._find_accounting_partner(self.picking_id.partner_id).id) or False
def _prepare_move_split_vals(self, uom_qty):
vals = super(StockMove, self)._prepare_move_split_vals(uom_qty)
vals['to_refund'] = self.to_refund
return vals
def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id):
self.ensure_one()
AccountMove = self.env['account.move']
+1 -1
View File
@@ -154,7 +154,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_adv_activity(self):
model = self.env['mail.test.activity']
with self.assertQueryCount(admin=8, emp=8): # test_mail only: 8 - 8
with self.assertQueryCount(admin=9, emp=9): # test_mail only: 8 - 8
model.create({'name': 'Test'})
@users('admin', 'emp')
+4 -1
View File
@@ -744,6 +744,8 @@ class Database(http.Controller):
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
def restore(self, master_pwd, backup_file, name, copy=False):
try:
data_file = None
db.check_super(master_pwd)
with tempfile.NamedTemporaryFile(delete=False) as data_file:
backup_file.save(data_file)
db.restore_db(name, data_file.name, str2bool(copy))
@@ -752,7 +754,8 @@ class Database(http.Controller):
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
finally:
os.unlink(data_file.name)
if data_file:
os.unlink(data_file.name)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
def change_password(self, master_pwd, master_pwd_new):
+1 -1
View File
@@ -829,7 +829,7 @@
<t t-foreach="elements" t-as="el">
<t t-set="color" t-value="el[colorField] || 0"/>
<span t-attf-class="badge dropdown o_tag_color_#{color}" t-att-data-color="color" t-att-data-index="el_index" t-att-data-id="el.id">
<span class="o_badge_text"><t t-esc="el.display_name"/></span>
<span class="o_badge_text" t-att-title="el.display_name"><t t-esc="el.display_name"/></span>
<span t-if="!readonly" class="fa fa-times o_delete"/>
</span>
</t>
@@ -10680,6 +10680,31 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('widget many2many_tags', function (assert) {
assert.expect(1);
this.data.turtle.records[0].partner_ids = [2];
var form = createView({
View: FormView,
model: 'turtle',
data: this.data,
arch:'<form string="Turtles">' +
'<sheet>' +
'<field name="display_name"/>' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.deepEqual(
form.$('.o_field_many2manytags.o_field_widget .badge .o_badge_text').attr('title'),
'second record', 'the title should be filled in'
);
form.destroy();
});
QUnit.module('FieldStatus');
QUnit.test('static statusbar widget on many2one field', function (assert) {
@@ -248,6 +248,10 @@ function applyOverDescendants(node, func) {
func(node);
applyOverDescendants(node, func);
}
var $node = $(node);
if (node.nodeName === 'A' && $node.hasClass('btn') && !$node.children().length && $(node).parents('.o_outlook_hack').length) {
node = $(node).parents('.o_outlook_hack')[0];
}
node = node.nextSibling;
}
}
@@ -283,7 +287,7 @@ function classToStyle($editable) {
// Outlook
if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
var $hack = $('<table class="o_outlook_hack" style="display: inline-table;"><tr><td></td></tr></table>');
var $hack = $('<table class="o_outlook_hack" style="display: inline-table;vertical-align:middle"><tr><td></td></tr></table>');
$hack.find('td')
.attr('height', $target.outerHeight())
.css({
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class Attachment(models.Model):
@@ -10,3 +10,7 @@ class Attachment(models.Model):
# related for backward compatibility with saas-6
website_url = fields.Char(string="Website URL", related='local_url', deprecated=True)
@api.model
def get_serving_groups(self):
return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
-1
View File
@@ -791,7 +791,6 @@ class Page(models.Model):
@api.multi
def write(self, vals):
self.ensure_one()
if 'url' in vals and not vals['url'].startswith('/'):
vals['url'] = '/' + vals['url']
result = super(Page, self).write(vals)
+4 -3
View File
@@ -16,7 +16,7 @@ except ImportError:
from datetime import datetime
from subprocess import Popen, PIPE
from collections import OrderedDict
from odoo import fields, tools
from odoo import fields, tools, SUPERUSER_ID
from odoo.tools.pycompat import string_types, to_text
from odoo.http import request
from odoo.modules.module import get_resource_path
@@ -239,10 +239,11 @@ class AssetsBundle(object):
self.env.cr.execute("""
SELECT max(id)
FROM ir_attachment
WHERE url like %s
WHERE create_uid = %s
AND url like %s
GROUP BY datas_fname
ORDER BY datas_fname
""", [url_pattern])
""", [SUPERUSER_ID, url_pattern])
attachment_ids = [r[0] for r in self.env.cr.fetchall()]
return self.env['ir.attachment'].sudo().browse(attachment_ids)
+11 -2
View File
@@ -5,13 +5,12 @@ import odoo
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import MissingError, UserError, ValidationError, AccessError
from odoo.tools.safe_eval import safe_eval, test_python_expr
from odoo.tools import pycompat
from odoo.tools import pycompat, wrap_module
from odoo.http import request
import base64
from collections import defaultdict
import datetime
import dateutil
import logging
import time
@@ -19,6 +18,16 @@ from pytz import timezone
_logger = logging.getLogger(__name__)
# build dateutil helper, starting with the relevant *lazy* imports
import dateutil
import dateutil.parser
import dateutil.relativedelta
import dateutil.rrule
import dateutil.tz
mods = {'parser', 'relativedelta', 'rrule', 'tz'}
attribs = {atr for m in mods for atr in getattr(dateutil, m).__all__}
dateutil = wrap_module(dateutil, mods | attribs)
class IrActions(models.Model):
_name = 'ir.actions.actions'
+1 -1
View File
@@ -209,7 +209,7 @@ class IrActionsReport(models.Model):
if landscape is None and specific_paperformat_args and specific_paperformat_args.get('data-report-landscape'):
landscape = specific_paperformat_args.get('data-report-landscape')
command_args = []
command_args = ['--disable-local-file-access']
if set_viewport_size:
command_args.extend(['--viewport-size', landscape and '1024x1280' or '1280x1024'])
+22 -1
View File
@@ -11,7 +11,7 @@ from collections import defaultdict
import uuid
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessError
from odoo.exceptions import AccessError, ValidationError
from odoo.tools import config, human_size, ustr, html_escape
from odoo.tools.mimetypes import guess_mimetype
@@ -263,6 +263,15 @@ class IrAttachment(models.Model):
index_content = b"\n".join(words).decode('ascii')
return index_content
@api.model
def get_serving_groups(self):
""" An ir.attachment record may be used as a fallback in the
http dispatch if its type field is set to "binary" and its url
field is set as the request's url. Only the groups returned by
this method are allowed to create and write on such records.
"""
return ['base.group_system']
name = fields.Char('Attachment Name', required=True)
datas_fname = fields.Char('File Name')
description = fields.Text('Description')
@@ -299,6 +308,18 @@ class IrAttachment(models.Model):
self._table, ['res_model', 'res_id'])
return res
@api.one
@api.constrains('type', 'url')
def _check_serving_attachments(self):
# restrict writing on attachments that could be served by the
# ir.http's dispatch exception handling
if self.env.user._is_superuser():
return
if self.type == 'binary' and self.url:
has_group = self.env.user.has_group
if not any([has_group(g) for g in self.get_serving_groups()]):
raise ValidationError("Sorry, you are not allowed to write on this document")
@api.model
def check(self, mode, values=None):
"""Restricts the access to an ir.attachment, according to referred model
+6 -1
View File
@@ -567,7 +567,11 @@ class IrModelFields(models.Model):
failed_dependencies = []
for rec in self:
model = self.env[rec.model]
field = model._fields[rec.name]
if rec.name in model._fields:
field = model._fields[rec.name]
else:
# field hasn't been loaded (yet?)
continue
for dependant, path in model._field_triggers.get(field, ()):
if dependant.manual:
failed_dependencies.append((field, dependant))
@@ -764,6 +768,7 @@ class IrModelFields(models.Model):
'readonly': bool(field.readonly),
'required': bool(field.required),
'selectable': bool(field.search or field.store),
'size': getattr(field, 'size', None),
'translate': bool(field.translate),
'relation_field': field.inverse_name if field.type == 'one2many' else None,
'relation_table': field.relation if field.type == 'many2many' else None,
+1
View File
@@ -188,6 +188,7 @@ class Module(models.Model):
'doctitle_xform': False,
'output_encoding': 'unicode',
'xml_declaration': False,
'file_insertion_enabled': False,
}
output = publish_string(source=module.description or '', settings_overrides=overrides, writer=MyWriter())
module.description_html = tools.html_sanitize(output)
+14 -11
View File
@@ -346,6 +346,19 @@ class Users(models.Model):
def onchange_parent_id(self):
return self.mapped('partner_id').onchange_parent_id()
def _read_from_database(self, field_names, inherited_field_names=[]):
super(Users, self)._read_from_database(field_names, inherited_field_names)
canwrite = self.check_access_rights('write', raise_exception=False)
if not canwrite and set(USER_PRIVATE_FIELDS).intersection(field_names):
for record in self:
for f in USER_PRIVATE_FIELDS:
try:
record._cache[f]
record._cache[f] = '********'
except Exception:
# skip SpecialValue (e.g. for missing record or access right)
pass
@api.multi
@api.constrains('company_id', 'company_ids')
def _check_company(self):
@@ -376,17 +389,7 @@ class Users(models.Model):
# safe fields only, so we read as super-user to bypass access rights
self = self.sudo()
result = super(Users, self).read(fields=fields, load=load)
canwrite = self.env['ir.model.access'].check('res.users', 'write', False)
if not canwrite:
for vals in result:
if vals['id'] != self._uid:
for key in USER_PRIVATE_FIELDS:
if key in vals:
vals[key] = '********'
return result
return super(Users, self).read(fields=fields, load=load)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+1
View File
@@ -1498,6 +1498,7 @@ def db_filter(dbs, httprequest=None):
if d == "www" and r:
d = r.partition('.')[0]
if odoo.tools.config['dbfilter']:
d, h = re.escape(d), re.escape(h)
r = odoo.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
dbs = [i for i in dbs if re.match(r, i)]
elif odoo.tools.config['db_name']:
+9 -4
View File
@@ -3582,6 +3582,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if table not in query.tables:
query.tables.append(table)
if self._transient:
# One single implicit access rule for transient models: owner only!
# This is ok because we assert that TransientModels always have
# log_access enabled, so that 'create_uid' is always there.
domain = [('create_uid', '=', self._uid)]
tquery = self._where_calc(domain, active_test=False)
apply_rule(tquery.where_clause, tquery.where_clause_params, tquery.tables)
return
# apply main rules on the object
Rule = self.env['ir.rule']
where_clause, where_params, tables = Rule.domain_get(self._name, mode)
@@ -3728,10 +3737,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
"""
self.sudo(access_rights_uid or self._uid).check_access_rights('read')
# For transient models, restrict access to the current user, except for the super-user
if self.is_transient() and self._log_access and self._uid != SUPERUSER_ID:
args = expression.AND(([('create_uid', '=', self._uid)], args or []))
if expression.is_false(self, args):
# optimization: no need to query, as no record satisfies the domain
return 0 if count else []
+1 -1
View File
@@ -45,7 +45,7 @@ class _Cleaner(clean.Cleaner):
_style_whitelist = [
'font-size', 'font-family', 'font-weight', 'background-color', 'color', 'text-align',
'line-height', 'letter-spacing', 'text-transform', 'text-decoration', 'opacity',
'float', 'vertical-align',
'float', 'vertical-align', 'display',
'padding', 'padding-top', 'padding-left', 'padding-bottom', 'padding-right',
'margin', 'margin-top', 'margin-left', 'margin-bottom', 'margin-right',
# box model
+20
View File
@@ -1221,3 +1221,23 @@ pickle.load = _pickle_load
pickle.loads = lambda text, encoding='ASCII': _pickle_load(io.BytesIO(text), encoding=encoding)
pickle.dump = pickle_.dump
pickle.dumps = pickle_.dumps
def wrap_module(module, attr_list):
"""Helper for wrapping a package/module to expose selected attributes
:param Module module: the actual package/module to wrap, as returned by ``import <module>``
:param iterable attr_list: a global list of attributes to expose, usually the top-level
attributes and their own main attributes. No support for hiding attributes in case
of name collision at different levels.
"""
attr_list = set(attr_list)
class WrappedModule(object):
def __getattr__(self, attrib):
if attrib in attr_list:
target = getattr(module, attrib)
if isinstance(target, types.ModuleType):
return wrap_module(target, attr_list)
return target
raise AttributeError(attrib)
# module and attr_list are in the closure
return WrappedModule()