[MERGE] forward port branch saas-11.3 up to af35aea6b0
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user