[MERGE] forward port branch saas-11.3 up to 4850fb0838

This commit is contained in:
Christophe Simonis
2018-09-07 14:43:48 +02:00
98 changed files with 1417 additions and 343 deletions
+5 -2
View File
@@ -1571,8 +1571,11 @@ class AccountPartialReconcile(models.Model):
if rec.full_reconcile_id:
full_to_unlink |= rec.full_reconcile_id
#reverse the tax basis move created at the reconciliation time
move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self._ids)])
move.reverse_moves()
for move in self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self._ids)]):
if move.date > (move.company_id.period_lock_date or '0000-00-00'):
move.reverse_moves(date=move.date)
else:
move.reverse_moves()
res = super(AccountPartialReconcile, self).unlink()
if full_to_unlink:
full_to_unlink.unlink()
@@ -108,7 +108,7 @@ class AccountInvoiceReport(models.Model):
1 AS nbr,
ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
ai.partner_bank_id,
SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty,
SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty,
SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
SUM(ABS(ail.price_subtotal_signed)) / CASE
WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
@@ -118,7 +118,7 @@ class AccountInvoiceReport(models.Model):
ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
count(*) * invoice_type.sign AS residual,
ai.commercial_partner_id as commercial_partner_id,
partner.country_id
coalesce(partner.country_id, partner_ai.country_id) AS country_id
"""
return select_str
@@ -127,6 +127,7 @@ class AccountInvoiceReport(models.Model):
FROM account_invoice_line ail
JOIN account_invoice ai ON ai.id = ail.invoice_id
JOIN res_partner partner ON ai.commercial_partner_id = partner.id
JOIN res_partner partner_ai ON ai.partner_id = partner_ai.id
LEFT JOIN product_product pr ON pr.id = ail.product_id
left JOIN product_template pt ON pt.id = pr.product_tmpl_id
LEFT JOIN uom_uom u ON u.id = ail.uom_id
@@ -137,7 +138,11 @@ class AccountInvoiceReport(models.Model):
WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text])
THEN -1
ELSE 1
END) AS sign
END) AS sign,(CASE
WHEN ai.type::text = ANY (ARRAY['out_refund'::character varying::text, 'in_invoice'::character varying::text])
THEN -1
ELSE 1
END) AS sign_qty
FROM account_invoice ai
) AS invoice_type ON invoice_type.id = ai.id
"""
@@ -149,7 +154,7 @@ class AccountInvoiceReport(models.Model):
ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id,
ai.fiscal_position_id, ai.user_id, ai.company_id, ai.type, invoice_type.sign, ai.state, pt.categ_id,
ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed,
ai.amount_total_company_signed, ai.commercial_partner_id, partner.country_id
ai.amount_total_company_signed, ai.commercial_partner_id, coalesce(partner.country_id, partner_ai.country_id)
"""
return group_by_str
@@ -872,8 +872,14 @@ var StatementModel = BasicModel.extend({
}
}
});
total = Math.round(total*1000)/1000 || 0;
amount_currency = Math.round(amount_currency);
var company_currency = session.get_currency(line.st_line.currency_id);
var company_precision = company_currency && company_currency.digits[1] || 2;
total = utils.round_precision(total*1000, company_precision)/1000 || 0;
if(isOtherCurrencyId){
var other_currency = session.get_currency(isOtherCurrencyId);
var other_precision = other_currency && other_currency.digits[1] || 2;
amount_currency = utils.round_precision(amount_currency, other_precision)
}
line.balance = {
amount: total,
amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),
+2 -4
View File
@@ -1788,8 +1788,6 @@ class Meeting(models.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]
if 'UNTIL' not in rule_str and 'COUNT' not in rule_str:
rule_str += ';COUNT=100'
return rule_str
@@ -210,6 +210,12 @@ class DeliveryCarrier(models.Model):
carrier.product_id.list_price = carrier.fixed_price
def fixed_rate_shipment(self, order):
carrier = self._match_address(order.partner_shipping_id)
if not carrier:
return {'success': False,
'price': 0.0,
'error_message': _('Error: this delivery method is not available for this address.'),
'warning_message': False}
price = self.fixed_price
if self.company_id.currency_id.id != order.currency_id.id:
price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price)
+1 -1
View File
@@ -47,7 +47,7 @@ class ProviderGrid(models.Model):
if not carrier:
return {'success': False,
'price': 0.0,
'error_message': _('Error: no matching grid.'),
'error_message': _('Error: this delivery method is not available for this address.'),
'warning_message': False}
try:
@@ -25,12 +25,12 @@ var MyAttendances = AbstractAction.extend({
args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name']],
})
.then(function (res) {
if (_.isEmpty(res) ) {
self.$('.o_hr_attendance_employee').append(_t("Error : Could not find employee linked to user"));
return;
}
self.employee = res[0];
self.$el.html(QWeb.render("HrAttendanceMyMainMenu", {widget: self}));
if (_.isEmpty(res) ) {
return;
}
});
return $.when(def, this._super.apply(this, arguments));
@@ -2,14 +2,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.exceptions import UserError, AccessError
class GamificationBadgeUserWizard(models.TransientModel):
_inherit = 'gamification.badge.user.wizard'
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', store=True)
user_id = fields.Many2one('res.users', string='User',
related='employee_id.user_id', store=True, readonly=True)
# TODO 12.0/master remove this hack by changing the model
@api.model
def create(self, values):
employee = self.env['hr.employee'].browse(values['employee_id'])
values['user_id'] = employee.user_id.id
try:
return super(GamificationBadgeUserWizard, self).create(values)
except AccessError:
# an employee can not write on another employee
# force sudo because of related
return super(GamificationBadgeUserWizard, self.sudo()).create(values)
@api.multi
def action_grant_badge(self):
+110 -58
View File
@@ -10,7 +10,7 @@ from datetime import timedelta, datetime, time
from pytz import timezone, UTC
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare
from odoo.tools.translate import _
@@ -18,27 +18,47 @@ from odoo.addons.resource.models.resource import float_to_time
_logger = logging.getLogger(__name__)
class HolidaysRequest(models.Model):
"""
Here are the rights associated with the flow of leave requests
State Groups Restriction
============================================================================================================
Draft Anybody Own Request only and state in [confirm, refuse]
Holiday Manager State in [confirm, refuse]
Confirm All State = draft
Validate1 Holiday Officer Not his own Request and state = confirm
Holiday Manager Not his own Request or has no manager and State = confirm
Validate Holiday Officer Not his own Request and state = confirm
Holiday Manager Not his own Request or has no manager and State in [validate1, confirm]
Refuse Holiday Officer State in [confirm, validate, validate1]
Holiday Manager State in [confirm, validate, validate1]
============================================================================================================
class HolidaysRequest(models.Model):
""" Leave Requests Access specifications
- a regular employee / user
- can see all leaves;
- cannot see name field of leaves belonging to other user as it may contain
private information that we don't want to share to other people than
HR people;
- can modify only its own not validated leaves (except writing on state to
bypass approval);
- can discuss on its leave requests;
- can reset only its own leaves;
- cannot validate any leaves;
- an Officer
- can see all leaves;
- can validate "HR" single validation leaves from people if
- he is the employee manager;
- he is the department manager;
- he is member of the same department;
- target employee has no manager and no department manager;
- can validate "Manager" single validation leaves from people if
- he is the employee manager;
- he is the department manager;
- target employee has no manager and no department manager;
- can first validate "Both" double validation leaves from people like "HR"
single validation, moving the leaves to validate1 state;
- cannot validate its own leaves;
- can reset only its own leaves;
- can refuse all leaves;
- a Manager
- can do everything he wants
On top of that multicompany rules apply based on company defined on the
leave request leave type.
"""
_name = "hr.leave"
_description = "Leave"
_order = "date_from desc"
_inherit = ['mail.thread', 'mail.activity.mixin']
_mail_post_access = 'read'
def _default_domain_holiday_status(self):
if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
@@ -58,7 +78,6 @@ class HolidaysRequest(models.Model):
def _default_employee(self):
return self.env.context.get('default_employee_id') or self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
name = fields.Char('Description')
state = fields.Selection([
('draft', 'To Submit'),
@@ -297,25 +316,28 @@ class HolidaysRequest(models.Model):
holiday.number_of_hours = holiday.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day
@api.multi
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
""" User can reset a leave request if it is its own leave request
or if he is an Hr Manager.
"""
user = self.env.user
group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager')
for holiday in self:
if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user:
try:
holiday._check_approval_update('draft')
except (AccessError, UserError):
holiday.can_reset = False
else:
holiday.can_reset = True
@api.depends('employee_id', 'department_id')
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_approve(self):
""" User can only approve a leave request if it is not his own
Exception : User is holiday manager and has no manager
"""
for holiday in self:
# User is holiday manager and has no manager
manager = self.user_has_groups('hr_holidays.group_hr_holidays_manager')
holiday.can_approve = (holiday.employee_id.user_id.id != self.env.uid) or manager
try:
if holiday.state == 'confirm' and holiday.holiday_status_id.validation_type == 'both':
holiday._check_approval_update('validate1')
else:
holiday._check_approval_update('validate')
except (AccessError, UserError):
holiday.can_approve = False
else:
holiday.can_approve = True
@api.constrains('date_from', 'date_to')
def _check_date(self):
@@ -382,8 +404,9 @@ class HolidaysRequest(models.Model):
dto = fields.Datetime.from_string(leave.date_to)
if dfrom and dto and (dfrom < vstart or dto > vstop):
raise UserError(_('You can take %s only between %s and %s') % (leave.holiday_status_id.display_name, \
leave.holiday_status_id.validity_start, leave.holiday_status_id.validity_stop))
raise UserError(
_('You can take %s only between %s and %s') % (
leave.holiday_status_id.display_name, leave.holiday_status_id.validity_start, leave.holiday_status_id.validity_stop))
@api.model
def create(self, values):
@@ -398,9 +421,29 @@ class HolidaysRequest(models.Model):
holiday.activity_update()
return holiday
def _read_from_database(self, field_names, inherited_field_names=[]):
if 'name' in field_names and 'employee_id' not in field_names:
field_names.append('employee_id')
super(HolidaysRequest, self)._read_from_database(field_names, inherited_field_names)
if 'name' in field_names:
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
return
current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
for record in self:
emp_id = record._cache.get('employee_id', [False])[0]
if emp_id != current_employee.id:
try:
record._cache['name']
record._cache['name'] = '*****'
except Exception:
# skip SpecialValue (e.g. for missing record or access right)
pass
@api.multi
def write(self, values):
employee_id = values.get('employee_id', False)
if values.get('state'):
self._check_approval_update(values['state'])
result = super(HolidaysRequest, self).write(values)
self.add_follower(employee_id)
if 'employee_id' in values:
@@ -494,8 +537,6 @@ class HolidaysRequest(models.Model):
@api.multi
def action_draft(self):
for holiday in self:
if not holiday.can_reset:
raise UserError(_('Only an HR Manager or the concerned employee can reset the leave request to draft.'))
if holiday.state not in ['confirm', 'refuse']:
raise UserError(_('Leave request state must be "Refused" or "To Approve" in order to be reset to draft.'))
holiday.write({
@@ -522,22 +563,10 @@ class HolidaysRequest(models.Model):
def action_approve(self):
# if validation_type == 'both': this method is the first approval approval
# if validation_type != 'both': this method calls action_validate() below
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
raise UserError(_('Only an HR Officer or Manager can approve leave requests.'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
if any(holiday.state != 'confirm' for holiday in self):
raise UserError(_('Leave request must be confirmed ("To Approve") in order to approve it.'))
for holiday in self:
validation_type = holiday.holiday_status_id.validation_type
manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id
if (validation_type in ['hr', 'both']) and (manager and manager != current_employee)\
and not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('You must be %s manager to approve this leave') % (holiday.employee_id.name))
elif validation_type == 'manager' and not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('You must be a Human Resource Manager to approve this leave.'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id})
self.filtered(lambda hol: not hol.validation_type == 'both').action_validate()
self.activity_update()
@@ -545,18 +574,10 @@ class HolidaysRequest(models.Model):
@api.multi
def action_validate(self):
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
raise UserError(_('Only an HR Officer or Manager can approve leave requests.'))
if any(not holiday.can_approve for holiday in self):
raise UserError(_('Only your manager can approve your leave requests'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
for holiday in self:
if holiday.state not in ['confirm', 'validate1']:
raise UserError(_('Leave request must be confirmed in order to approve it.'))
if holiday.state == 'validate1' and not holiday.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('Only an HR Manager can apply the second approval on leave requests.'))
holiday.write({'state': 'validate'})
if holiday.validation_type == 'both':
@@ -580,9 +601,6 @@ class HolidaysRequest(models.Model):
@api.multi
def action_refuse(self):
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
raise UserError(_('Only an HR Officer or Manager can refuse leave requests.'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
for holiday in self:
if holiday.state not in ['confirm', 'validate', 'validate1']:
@@ -601,6 +619,40 @@ class HolidaysRequest(models.Model):
self.activity_update()
return True
def _check_approval_update(self, state):
""" Check if target state is achievable. """
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
for holiday in self:
val_type = holiday.holiday_status_id.validation_type
if state == 'confirm':
continue
if state == 'draft':
if holiday.employee_id != current_employee and not is_manager:
raise UserError(_('Only a Leave Manager can reset other people leaves.'))
continue
if not is_officer:
raise UserError(_('Only a Leave Officer or Manager can approve or refuse leave requests.'))
if is_officer:
# use ir.rule based first access check: department, members, ... (see security.xml)
holiday.check_access_rule('write')
if holiday.employee_id == current_employee and not is_manager:
raise UserError(_('Only a Leave Manager can approve its own requests.'))
if (state == 'validate1' and val_type == 'both') or (state == 'validate' and val_type == 'manager'):
manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id
if (manager and manager != current_employee) and not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('You must be either %s\'s manager or Leave manager to approve this leave') % (holiday.employee_id.name))
if state == 'validate' and val_type == 'both':
if not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('Only an Leave Manager can apply the second approval on leave requests.'))
# ------------------------------------------------------------
# Activity methods
# ------------------------------------------------------------
@@ -11,7 +11,7 @@ from datetime import datetime, time
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
from odoo.tools.translate import _
from odoo.addons.resource.models.resource import HOURS_PER_DAY
@@ -20,25 +20,11 @@ _logger = logging.getLogger(__name__)
class HolidaysAllocation(models.Model):
"""
Here are the rights associated with the flow of allocation requests
State Groups Restriction
============================================================================================================
Draft Anybody Own Allocation only and state in [confirm, refuse]
Holiday Manager State in [confirm, refuse]
Confirm All State = draft
Validate1 Holiday Officer Not his own Allocation and state = confirm
Holiday Manager Not his own Allocation or has no manager and State = confirm
Validate Holiday Officer Not his own Allocation and state = confirm
Holiday Manager Not his own Allocation or has no manager and State in [validate1, confirm]
Refuse Holiday Officer State in [confirm, validate, validate1]
Holiday Manager State in [confirm, validate, validate1]
============================================================================================================
"""
""" Allocation Requests Access specifications: similar to leave requests """
_name = "hr.leave.allocation"
_description = "Leaves Allocation"
_inherit = ['mail.thread', 'mail.activity.mixin']
_mail_post_access = 'read'
def _default_domain_holiday_status_id(self):
if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
@@ -200,25 +186,28 @@ class HolidaysAllocation(models.Model):
holiday.number_of_days = number_of_days
@api.multi
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
""" User can reset a leave request if it is its own leave request
or if he is an Hr Manager.
"""
user = self.env.user
group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager')
for holiday in self:
if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user:
holiday.can_reset = True
for allocation in self:
try:
allocation._check_approval_update('draft')
except (AccessError, UserError):
allocation.can_reset = False
else:
allocation.can_reset = True
@api.depends('employee_id', 'department_id')
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_approve(self):
""" User can only approve a leave request if it is not his own
Exception : User is holiday manager and has no manager
"""
for holiday in self:
# User is holiday manager and has no manager
manager = self.user_has_groups('hr_holidays.group_hr_holidays_manager')
holiday.can_approve = (holiday.employee_id.user_id.id != self.env.uid) or manager
for allocation in self:
try:
if allocation.state == 'confirm' and allocation.holiday_status_id.validation_type == 'both':
allocation._check_approval_update('validate1')
else:
allocation._check_approval_update('validate')
except (AccessError, UserError):
allocation.can_approve = False
else:
allocation.can_approve = True
@api.onchange('holiday_type')
def _onchange_type(self):
@@ -312,6 +301,8 @@ class HolidaysAllocation(models.Model):
@api.multi
def write(self, values):
employee_id = values.get('employee_id', False)
if values.get('state'):
self._check_approval_update(values['state'])
result = super(HolidaysAllocation, self).write(values)
self.add_follower(employee_id)
return result
@@ -347,8 +338,6 @@ class HolidaysAllocation(models.Model):
@api.multi
def action_draft(self):
for holiday in self:
if not holiday.can_reset:
raise UserError(_('Only an HR Manager or the concerned employee can reset to draft.'))
if holiday.state not in ['confirm', 'refuse']:
raise UserError(_('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.'))
holiday.write({
@@ -375,21 +364,10 @@ class HolidaysAllocation(models.Model):
def action_approve(self):
# if validation_type == 'both': this method is the first approval approval
# if validation_type != 'both': this method calls action_validate() below
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
raise UserError(_('Only an HR Officer or Manager can approve leave requests.'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
if any(holiday.state != 'confirm' for holiday in self):
raise UserError(_('Leave request must be confirmed ("To Approve") in order to approve it.'))
for holiday in self:
validation_type = holiday.holiday_status_id.validation_type
manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id
if (validation_type in ['hr', 'both']) and (manager and manager != current_employee)\
and not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('You must be %s manager to approve this leave') % (holiday.employee_id.name))
elif validation_type == 'manager' and not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('You must be a Human Resource Manager to approve this Leave.'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id})
self.filtered(lambda hol: not hol.validation_type == 'both').action_validate()
@@ -398,18 +376,10 @@ class HolidaysAllocation(models.Model):
@api.multi
def action_validate(self):
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
raise UserError(_('Only an HR Officer or Manager can approve leave requests.'))
if any(not holiday.can_approve for holiday in self):
raise UserError(_('Only your manager can approve your allocations'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
for holiday in self:
if holiday.state not in ['confirm', 'validate1']:
raise UserError(_('Leave request must be confirmed in order to approve it.'))
if holiday.state == 'validate1' and not holiday.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('Only an HR Manager can apply the second approval on leave requests.'))
holiday.write({'state': 'validate'})
if holiday.validation_type == 'both':
@@ -431,9 +401,6 @@ class HolidaysAllocation(models.Model):
@api.multi
def action_refuse(self):
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
raise UserError(_('Only an HR Officer or Manager can refuse leave requests.'))
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
for holiday in self:
if holiday.state not in ['confirm', 'validate', 'validate1']:
@@ -448,6 +415,40 @@ class HolidaysAllocation(models.Model):
self.activity_update()
return True
def _check_approval_update(self, state):
""" Check if target state is achievable. """
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
for holiday in self:
val_type = holiday.holiday_status_id.validation_type
if state == 'confirm':
continue
if state == 'draft':
if holiday.employee_id != current_employee and not is_manager:
raise UserError(_('Only a Leave Manager can reset other people leaves.'))
continue
if not is_officer:
raise UserError(_('Only a Leave Officer or Manager can approve or refuse leave requests.'))
if is_officer:
# use ir.rule based first access check: department, members, ... (see security.xml)
holiday.check_access_rule('write')
if holiday.employee_id == current_employee and not is_manager:
raise UserError(_('Only a Leave Manager can approve its own requests.'))
if (state == 'validate1' and val_type == 'both') or (state == 'validate' and val_type == 'manager'):
manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id
if (manager and manager != current_employee) and not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('You must be either %s\'s manager or Leave manager to approve this leave') % (holiday.employee_id.name))
if state == 'validate' and val_type == 'both':
if not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
raise UserError(_('Only an Leave Manager can apply the second approval on leave requests.'))
# ------------------------------------------------------------
# Activity methods
# ------------------------------------------------------------
+19 -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, tools
from odoo import api, fields, models, tools
class LeaveReport(models.Model):
@@ -78,3 +78,21 @@ class LeaveReport(models.Model):
from hr_leave as request) leaves
);
""")
def _read_from_database(self, field_names, inherited_field_names=[]):
if 'name' in field_names and 'employee_id' not in field_names:
field_names.append('employee_id')
super(LeaveReport, self)._read_from_database(field_names, inherited_field_names)
if 'name' in field_names:
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
return
current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
for record in self:
emp_id = record._cache.get('employee_id', [False])[0]
if emp_id != current_employee.id:
try:
record._cache['name']
record._cache['name'] = '*****'
except Exception:
# skip SpecialValue (e.g. for missing record or access right)
pass
@@ -18,64 +18,96 @@
</record>
<record id="hr_leave_rule_employee" model="ir.rule">
<field name="name">Employee Leaves</field>
<field name="name">Leaves: employee: read all</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_leave_rule_employee_write" model="ir.rule">
<field name="name">Employee Leaves Create, Write, Unlink</field>
<record id="hr_leave_rule_employee_update" model="ir.rule">
<field name="name">Leaves: employee: create/write/unlink not validated own</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="domain_force">[('employee_id.user_id','=',user.id), ('state', 'in', ['draft', 'confirm', 'cancel', 'refuse'])]</field>
<field name="perm_read" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_leave_rule_officer" model="ir.rule">
<field name="name">Leaves Officer</field>
<record id="hr_leave_rule_officer_read" model="ir.rule">
<field name="name">Leaves: officer: read all</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="domain_force">['|','|','|', ('employee_id.user_id', '=', user.id), ('department_id', '=', False), ('department_id.manager_id', '=', False), ('department_id.manager_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4,ref('hr_holidays.group_hr_holidays_user'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('hr_holidays.group_hr_holidays_user'))]"/>
</record>
<record id="hr_leave_rule_officer_update" model="ir.rule">
<field name="name">Leaves: officer: create/write/unlink own + managed employee + without department + same or managed department</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="domain_force">['|',
'|', ('employee_id.user_id', '=', user.id), ('employee_id.parent_id.user_id', '=', user.id),
'|', '|', ('department_id', '=', False), ('department_id.member_ids.user_id', 'in', [user.id]), ('department_id.manager_id.user_id', '=', user.id)]</field>
<field name="perm_read" eval="False"/>
<field name="groups" eval="[(4, ref('hr_holidays.group_hr_holidays_user'))]"/>
</record>
<record id="hr_leave_rule_manager" model="ir.rule">
<field name="name">Manager Leaves</field>
<field name="name">Leaves: manager: no limit</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_hr_holidays_manager'))]"/>
</record>
<record id="hr_leave_rule_multicompany" model="ir.rule">
<field name="name">Leaves: multi company global rule</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('holiday_status_id.company_id', '=', False), ('holiday_status_id.company_id', 'in', [user.company_id.id])]</field>
</record>
<record id="hr_leave_allocation_rule_employee" model="ir.rule">
<field name="name">Employee Allocations</field>
<field name="name">Allocations: employee: read own</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_leave_allocation_rule_employee_write" model="ir.rule">
<field name="name">Employee Allocations Create, Write, Unlink</field>
<record id="hr_leave_allocation_rule_employee_update" model="ir.rule">
<field name="name">Allocations: employee: create/write/unlink not validated own</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="domain_force">[('employee_id.user_id','=',user.id), ('state', 'in', ['draft', 'confirm', 'cancel', 'refuse'])]</field>
<field name="perm_read" eval="False"/>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="hr_leave_allocation_rule_officer" model="ir.rule">
<field name="name">Leaves Officer</field>
<record id="hr_leave_allocation_rule_officer_read" model="ir.rule">
<field name="name">Allocations: officer: read all</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="domain_force">['|','|','|', ('employee_id.user_id', '=', user.id), ('department_id', '=', False), ('department_id.manager_id', '=', False), ('department_id.manager_id.user_id', '=', user.id)]</field>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('hr_holidays.group_hr_holidays_user'))]"/>
</record>
<record id="hr_leave_allocation_rule_officer_update" model="ir.rule">
<field name="name">Allocations: officer: create/write/unlink own + managed employee + without department + same or managed department</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="domain_force">['|',
'|', ('employee_id.user_id', '=', user.id), ('employee_id.parent_id.user_id', '=', user.id),
'|', '|', ('department_id', '=', False), ('department_id.member_ids.user_id', 'in', [user.id]), ('department_id.manager_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4,ref('hr_holidays.group_hr_holidays_user'))]"/>
</record>
<record id="hr_leave_allocation_rule_manager" model="ir.rule">
<field name="name">Manager Allocations</field>
<field name="name">Allocations: manager: no limit</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_hr_holidays_manager'))]"/>
+1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_access_rights
from . import test_holidays_flow
from . import test_hr_leave_type
from . import test_accrual_allocations
+19 -11
View File
@@ -15,24 +15,27 @@ class TestHrHolidaysBase(common.TransactionCase):
group_employee_id = self.ref('base.group_user')
# Test users to use through the various tests
self.user_hruser_id = Users.create({
self.user_hruser = Users.create({
'name': 'Armande HrUser',
'login': 'Armande',
'email': 'armande.hruser@example.com',
'groups_id': [(6, 0, [group_employee_id, self.ref('hr_holidays.group_hr_holidays_user')])]
}).id
self.user_hrmanager_id = Users.create({
})
self.user_hruser_id = self.user_hruser.id
self.user_hrmanager = Users.create({
'name': 'Bastien HrManager',
'login': 'bastien',
'email': 'bastien.hrmanager@example.com',
'groups_id': [(6, 0, [group_employee_id, self.ref('hr_holidays.group_hr_holidays_manager')])]
}).id
self.user_employee_id = Users.create({
self.user_hrmanager_id = self.user_hrmanager
self.user_employee = Users.create({
'name': 'David Employee',
'login': 'david',
'email': 'david.employee@example.com',
'groups_id': [(6, 0, [group_employee_id])]
}).id
})
self.user_employee_id = self.user_employee.id
self.user_hrmanager_2_id = Users.create({
'name': 'Florence HrManager',
'login': 'florence',
@@ -50,21 +53,26 @@ class TestHrHolidaysBase(common.TransactionCase):
'name': 'Research and devlopment',
})
self.employee_emp_id = self.env['hr.employee'].create({
self.employee_emp = self.env['hr.employee'].create({
'name': 'David Employee',
'user_id': self.user_employee_id,
'department_id': self.rd_dept.id,
}).id
self.employee_hruser_id = self.env['hr.employee'].create({
})
self.employee_emp_id = self.employee_emp.id
self.employee_hruser = self.env['hr.employee'].create({
'name': 'Armande HrUser',
'user_id': self.user_hruser_id,
'department_id': self.rd_dept.id,
}).id
self.employee_hrmanager_id = self.env['hr.employee'].create({
})
self.employee_hruser_id = self.employee_hruser.id
self.employee_hrmanager = self.env['hr.employee'].create({
'name': 'Bastien HrManager',
'user_id': self.user_hrmanager_id,
'department_id': self.hr_dept.id,
}).id
})
self.employee_hrmanager_id = self.employee_hrmanager.id
self.employee_hrmanager_2_id = self.env['hr.employee'].create({
'name': 'Florence HrManager',
@@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import tests
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
from odoo.exceptions import AccessError, UserError
from odoo.tools import mute_logger
@tests.tagged('access_rights')
class TestLeavesRights(TestHrHolidaysBase):
def setUp(self):
super(TestLeavesRights, self).setUp()
self.leave_type = self.env['hr.leave.type'].create({
'name': 'Unlimited',
'limit': True,
'validation_type': 'hr',
})
self.rd_dept.manager_id = False
self.hr_dept.manager_id = False
self.employee_emp.parent_id = False
self.employee_leave = self.env['hr.leave'].sudo(self.user_employee_id).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_emp.department_id.id,
'employee_id': self.employee_emp.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_messaging_by_user(self):
""" User may communicate on its own leaves, even if validated """
self.employee_leave.sudo(self.user_employee_id).message_post(
body='I haz messaging',
subtype='mail.mt_comment',
message_type='comment'
)
self.employee_leave.sudo(self.user_hrmanager_id).action_approve()
self.employee_leave.sudo(self.user_employee_id).message_post(
body='I still haz messaging',
subtype='mail.mt_comment',
message_type='comment'
)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_read_by_user_other(self):
""" Users should be able to read other people requests except name field """
other_leave = self.env['hr.leave'].sudo(self.user_hruser).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
res = other_leave.sudo(self.user_employee_id).read(['number_of_days_temp', 'state', 'name'])
self.assertEqual(
res[0]['name'], '*****',
'Private information should have been stripped, received %s instead' % res[0]['name']
)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_read_by_user_other_browse(self):
""" Users should be able to browse other people requests except name field """
other_leave = self.env['hr.leave'].sudo(self.user_hruser).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
self.assertEqual(
other_leave.sudo(self.user_employee_id).name, '*****',
'Private information should have been stripped, received %s instead' % other_leave.sudo(self.user_employee_id).name
)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_read_by_user_own(self):
""" Users should be able to read name field of own requests """
res = self.employee_leave.read(['name', 'number_of_days_temp', 'state'])
self.assertEqual(res[0]['name'], 'Test')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_read_by_user_own_browse(self):
""" Users should be able to browse name field of own requests """
self.assertEqual(self.employee_leave.state, 'confirm')
self.assertEqual(self.employee_leave.name, 'Test')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_update_hr_by_user(self):
""" User may update its leave """
self.employee_leave.sudo(self.user_employee_id).write({'name': 'Crocodile Dundee is my man'})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_update_hr_by_user_other(self):
""" User cannot update other people leaves """
other_leave = self.env['hr.leave'].sudo(self.user_hruser).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
with self.assertRaises(AccessError):
other_leave.sudo(self.user_employee_id).write({'name': 'Crocodile Dundee is my man'})
# ----------------------------------------
# Reset
# ----------------------------------------
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_reset_by_manager(self):
""" Manager resets its own leaves """
manager_leave = self.env['hr.leave'].sudo(self.user_hrmanager).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hrmanager.department_id.id,
'employee_id': self.employee_hrmanager.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
manager_leave.sudo(self.user_hrmanager).action_draft()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_reset_by_manager_other(self):
""" Manager may not reset other leaves """
self.employee_leave.sudo(self.user_hrmanager).action_draft()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_reset_by_officer(self):
""" Officer resets its own leaves """
officer_leave = self.env['hr.leave'].sudo(self.user_hruser).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
officer_leave.sudo(self.user_hruser).action_draft()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_reset_by_officer_other(self):
""" Officer may not reset other leaves """
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_hruser).action_draft()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_reset_by_user(self):
""" User resets its own leaves """
self.employee_leave.sudo(self.user_employee_id).action_draft()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_reset_by_user_other(self):
""" User may not reset other leaves """
other_leave = self.env['hr.leave'].sudo(self.user_hruser).create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_hruser.department_id.id,
'employee_id': self.employee_hruser.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
with self.assertRaises(UserError):
other_leave.sudo(self.user_employee_id).action_draft()
# ----------------------------------------
# Validation: one validation, HR
# ----------------------------------------
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_manager(self):
""" Manager validates hr-only leaves """
self.employee_leave.sudo(self.user_hrmanager_id).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_officer_department(self):
""" Officer validates hr-only leaves for co-workers """
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_officer_no_department(self):
""" Officer validates hr-only leaves for workers from no department and with no manager """
self.employee_hruser.write({'department_id': False})
# TDE FIXME: not sure for this one
# self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_officer_other_department_with_manager(self):
""" Officer may not validate hr-only leaves for workers from another department that has a manager """
self.employee_hruser.write({'department_id': self.hr_dept.id})
with self.assertRaises(AccessError):
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_officer_other_department_wo_manager(self):
""" Officer may not validate hr-only leaves for workers from another department that has no manager """
self.employee_hruser.write({'department_id': self.hr_dept.id})
with self.assertRaises(AccessError):
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_officer_other_department_manager(self):
""" Officer validates hr-only leaves for workers from another department that he manages """
self.employee_hruser.write({'department_id': self.hr_dept.id})
self.employee_leave.sudo().department_id.write({'manager_id': self.employee_hruser.id})
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_hr_by_user(self):
""" User may not validate any leaves """
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_employee_id).action_approve()
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_employee_id).write({'state': 'validate'})
# ----------------------------------------
# Validation: one validation, manager
# ----------------------------------------
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_manager_by_manager(self):
""" Manager validates manager-only leaves """
self.leave_type.write({'validation_type': 'manager'})
self.employee_leave.sudo(self.user_hrmanager_id).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_manager_by_officer_department_manager(self):
""" Officer validates manager-only leaves for co-workers from department he manages"""
self.leave_type.write({'validation_type': 'manager'})
self.employee_hruser.write({'department_id': self.hr_dept.id})
self.employee_leave.sudo().department_id.write({'manager_id': self.employee_hruser.id})
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_manager_by_officer_department_manager_other(self):
""" Officer may not validates manager-only leaves for co-workers from department he does not"""
self.leave_type.write({'validation_type': 'manager'})
self.employee_hruser.write({'department_id': self.hr_dept.id})
self.employee_leave.sudo().department_id.write({'manager_id': self.employee_hrmanager.id})
with self.assertRaises(AccessError):
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_manager_by_officer_manager(self):
""" Officer validates manager-only leaves for co-workers that he manages"""
self.leave_type.write({'validation_type': 'manager'})
self.employee_emp.write({'parent_id': self.employee_hruser.id})
self.employee_leave.sudo(self.user_hruser).action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_manager_by_officer_manager_other(self):
""" Officer may not validate manager-only leaves for co-workers he does not manage"""
self.leave_type.write({'validation_type': 'manager'})
self.employee_emp.write({'parent_id': self.employee_hrmanager.id})
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_hruser).action_approve()
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_hruser).write({'state': 'validate'})
# ----------------------------------------
# Validation: double
# ----------------------------------------
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_both_by_manager(self):
""" Manager validates double validation leaves """
self.leave_type.write({'validation_type': 'both'})
self.employee_leave.sudo(self.user_hrmanager_id).action_approve()
self.employee_leave.sudo(self.user_hrmanager_id).action_validate()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_both_by_officer(self):
""" Officer may not validate double validation leaves """
self.leave_type.write({'validation_type': 'both'})
self.employee_leave.sudo(self.user_hruser).action_approve()
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_hruser).action_validate()
with self.assertRaises(UserError):
self.employee_leave.sudo(self.user_hruser).write({'state': 'validate'})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_validation_both_by_officer_and_manager(self):
""" Officer + Manager complete double validation leaves """
self.leave_type.write({'validation_type': 'both'})
self.employee_leave.sudo(self.user_hruser).action_approve()
self.employee_leave.sudo(self.user_hrmanager_id).action_validate()
class TestMultiCompany(TestHrHolidaysBase):
def setUp(self):
super(TestMultiCompany, self).setUp()
self.new_company = self.env['res.company'].create({
'name': 'Crocodile Dundee Company',
})
self.leave_type = self.env['hr.leave.type'].create({
'name': 'Unlimited - Company New',
'limit': True,
'company_id': self.new_company.id,
'validation_type': 'hr',
})
self.rd_dept.manager_id = False
self.hr_dept.manager_id = False
self.employee_leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.leave_type.id,
'department_id': self.employee_emp.department_id.id,
'employee_id': self.employee_emp.id,
'date_from': datetime.now(),
'date_to': datetime.now() + relativedelta(days=1),
'number_of_days_temp': 1,
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_other_company_user(self):
employee_leave = self.employee_leave.sudo(self.user_employee)
with self.assertRaises(AccessError):
name = employee_leave.name
with self.assertRaises(AccessError):
employee_leave.action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_other_company_officer(self):
employee_leave_hruser = self.employee_leave.sudo(self.user_hruser)
with self.assertRaises(AccessError):
name = employee_leave_hruser.name
with self.assertRaises(AccessError):
employee_leave_hruser.action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_other_company_manager(self):
employee_leave_hrmanager = self.employee_leave.sudo(self.user_hrmanager)
with self.assertRaises(AccessError):
name = employee_leave_hrmanager.name
with self.assertRaises(AccessError):
employee_leave_hrmanager.action_approve()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_no_company_user(self):
self.leave_type.write({'company_id': False})
employee_leave = self.employee_leave.sudo(self.user_employee)
name = employee_leave.name
with self.assertRaises(UserError):
employee_leave.action_approve()
self.assertEqual(employee_leave.state, 'confirm')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_no_company_officer(self):
self.leave_type.write({'company_id': False})
employee_leave_hruser = self.employee_leave.sudo(self.user_hruser)
name = employee_leave_hruser.name
employee_leave_hruser.action_approve()
self.assertEqual(employee_leave_hruser.state, 'validate')
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_access_no_company_manager(self):
self.leave_type.write({'company_id': False})
employee_leave_hrmanager = self.employee_leave.sudo(self.user_hrmanager)
name = employee_leave_hrmanager.name
employee_leave_hrmanager.action_approve()
self.assertEqual(employee_leave_hrmanager.state, 'validate')
+1 -47
View File
@@ -72,15 +72,6 @@ class TestHolidaysFlow(TestHrHolidaysBase):
# Employee creates a leave request for another employee -> should crash
HolidaysEmployeeGroup = Requests.sudo(self.user_employee_id)
with self.assertRaises(ValidationError):
HolidaysEmployeeGroup.create({
'name': 'Hol10',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_status_1.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': datetime.today(),
'number_of_days_temp': 1,
})
Requests.search([('name', '=', 'Hol10')]).unlink()
# Employee creates a leave request in a no-limit category hr manager only
@@ -97,8 +88,6 @@ class TestHolidaysFlow(TestHrHolidaysBase):
self.assertEqual(hol1_user_group.state, 'confirm', 'hr_holidays: newly created leave request should be in confirm state')
# Employee validates its leave request -> should not work
with self.assertRaises(UserError):
hol1_employee_group.action_approve()
self.assertEqual(hol1_manager_group.state, 'confirm', 'hr_holidays: employee should not be able to validate its own leave request')
# HrUser validates the employee leave request -> should work
@@ -119,14 +108,8 @@ class TestHolidaysFlow(TestHrHolidaysBase):
self.assertEqual(hol12_user_group.state, 'confirm', 'hr_holidays: newly created leave request should be in confirm state')
# Employee validates its leave request -> should not work
with self.assertRaises(UserError):
hol12_employee_group.action_approve()
self.assertEqual(hol12_user_group.state, 'confirm', 'hr_holidays: employee should not be able to validate its own leave request')
# HrUser validates the employee leave request -> should not work
with self.assertRaises(UserError):
hol12_user_group.action_approve()
# HrManager validate the employee leave request
hol12_manager_group.action_approve()
self.assertEqual(hol1_user_group.state, 'validate', 'hr_holidays: validates leave request should be in validate state')
@@ -169,9 +152,7 @@ class TestHolidaysFlow(TestHrHolidaysBase):
})
# HrUser validates the first step
aloc1_user_group.action_approve()
# HrUser validates the second step -> should crash
with self.assertRaises(UserError):
aloc1_user_group.action_validate()
# HrManager validates the second step
aloc1_user_group.sudo(self.user_hrmanager_id).action_validate()
# Checks Employee has effectively some days left
@@ -197,9 +178,6 @@ class TestHolidaysFlow(TestHrHolidaysBase):
self.assertEqual(hol2.state, 'validate1',
'hr_holidays: first validation should lead to validate1 state')
# HrUser validates the second step -> should crash
with self.assertRaises(UserError):
hol2_user_group.action_validate()
# HrManager validates the second step
hol2_user_group.sudo(self.user_hrmanager_id).action_validate()
self.assertEqual(hol2.state, 'validate',
@@ -214,9 +192,6 @@ class TestHolidaysFlow(TestHrHolidaysBase):
# Check left days: 2 days left again
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0)
# Annoyed, HrUser tries to fix its error and tries to reset the leave request -> does not work, only HrManager
with self.assertRaises(UserError):
hol2_user_group.action_draft()
self.assertEqual(hol2.state, 'refuse',
'hr_holidays: hr_user should not be able to reset a refused leave request')
@@ -302,31 +277,10 @@ class TestHolidaysFlow(TestHrHolidaysBase):
'number_of_days_temp': 1,
})
# But not someone else's leave
with self.assertRaises(AccessError):
hol42.sudo(self.user_employee_id).action_draft()
# An officer should not be able to reset someone else's leave
with self.assertRaises(UserError):
hol42.sudo(self.user_hruser_id).action_draft()
# A manager should be able to reset someone else's leave
hol42.action_draft()
hol42.unlink()
# Officer should not be able to approve it's own leave
hol50 = HolidaysEmployeeGroup.sudo(self.user_hruser_id).create({
'name': 'Hol50',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.holidays_status_1.id,
'date_from': (datetime.today() + relativedelta(days=15)).strftime('%Y-%m-%d %H:%M'),
'date_to': (datetime.today() + relativedelta(days=16)),
'number_of_days_temp': 1,
})
with self.assertRaises(UserError):
hol50.action_approve()
# Manager should be able to approve it's own leave
hol51 = HolidaysEmployeeGroup.sudo(self.user_hrmanager_2_id).create({
'name': 'Hol51',
@@ -881,7 +881,7 @@
<field name="inherit_id" ref="hr.view_employee_filter"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='department_id']" position="after">
<filter name="newly_hired_employee" string="New Hired" domain="[('newly_hired_employee', '=', True)]"/>
<filter name="newly_hired_employee" string="New Hired" domain="[('newly_hired_employee', '=', True)]" groups="hr_recruitment.group_hr_recruitment_user"/>
</xpath>
</field>
</record>
@@ -70,7 +70,7 @@
<pivot string="Timesheet">
<field name="employee_id" type="row"/>
<field name="date" interval="month" type="col"/>
<field name="unit_amount" type="measure"/>
<field name="unit_amount" type="measure" widget="float_time"/>
</pivot>
</field>
</record>
@@ -58,7 +58,7 @@ index_template = """
</p>
%s
<p>
The PosBox software installed on this posbox is <b>version 17 BETA</b>,
The PosBox software installed on this posbox is <b>version 17</b>,
the posbox version number is independent from Odoo. You can upgrade
the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>.
</p>
+12 -12
View File
@@ -352,7 +352,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Company Withholding Tax 1% (Transportation)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="1"/>
<field name="amount" eval="-1"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -364,7 +364,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Company Withholding Tax 2% (Advertising)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="2"/>
<field name="amount" eval="-2"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -376,7 +376,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Company Withholding Tax 3% (Service)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="3"/>
<field name="amount" eval="-3"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -388,7 +388,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Company Withholding Tax 5% (Rental)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="5"/>
<field name="amount" eval="-5"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -400,7 +400,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Personal Withholding Tax 1% (Transportation)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="1"/>
<field name="amount" eval="-1"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -412,7 +412,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Personal Withholding Tax 2% (Advertising)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="2"/>
<field name="amount" eval="-2"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -424,7 +424,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Personal Withholding Tax 3% (Service)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="3"/>
<field name="amount" eval="-3"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -436,7 +436,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Personal Withholding Tax 5% (Rental)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="5"/>
<field name="amount" eval="-5"/>
<field name="account_id" ref="a_wht"/>
<field name="refund_account_id" ref="a_wht"/>
<field name="type_tax_use">purchase</field>
@@ -448,7 +448,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Withholding Income Tax 1% (Transportation)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="1"/>
<field name="amount" eval="-1"/>
<field name="account_id" ref="a_wht_income"/>
<field name="refund_account_id" ref="a_wht_income"/>
<field name="type_tax_use">sale</field>
@@ -459,7 +459,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Withholding Income Tax 2% (Advertising)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="2"/>
<field name="amount" eval="-2"/>
<field name="account_id" ref="a_wht_income"/>
<field name="refund_account_id" ref="a_wht_income"/>
<field name="type_tax_use">sale</field>
@@ -470,7 +470,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Withholding Income Tax 3% (Service)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="3"/>
<field name="amount" eval="-3"/>
<field name="account_id" ref="a_wht_income"/>
<field name="refund_account_id" ref="a_wht_income"/>
<field name="type_tax_use">sale</field>
@@ -481,7 +481,7 @@
<field name="chart_template_id" ref="chart"/>
<field name="name">Withholding Income Tax 5% (Rental)</field>
<field name="amount_type">percent</field>
<field name="amount" eval="5"/>
<field name="amount" eval="-5"/>
<field name="account_id" ref="a_wht_income"/>
<field name="refund_account_id" ref="a_wht_income"/>
<field name="type_tax_use">sale</field>
+7 -1
View File
@@ -146,7 +146,13 @@ class MailActivity(models.Model):
def _onchange_activity_type_id(self):
if self.activity_type_id:
self.summary = self.activity_type_id.summary
self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days))
tz = self.user_id.sudo().tz
if tz:
today_utc = pytz.UTC.localize(datetime.utcnow())
today = today_utc.astimezone(pytz.timezone(tz))
else:
today = datetime.now()
self.date_deadline = (today + timedelta(days=self.activity_type_id.days))
@api.onchange('recommended_activity_type_id')
def _onchange_recommended_activity_type_id(self):
+2 -1
View File
@@ -27,6 +27,7 @@ from werkzeug import urls
from odoo import _, api, exceptions, fields, models, tools
from odoo.tools import pycompat, ustr
from odoo.tools.misc import clean_context
from odoo.tools.safe_eval import safe_eval
@@ -306,7 +307,7 @@ class MailThread(models.AbstractModel):
# Perform the tracking
if tracked_fields:
track_self.message_track(tracked_fields, initial_values)
track_self.with_context(clean_context(self._context)).message_track(tracked_fields, initial_values)
return result
+6 -2
View File
@@ -8,6 +8,10 @@ class Event(models.Model):
_inherit = "event.event"
def action_mass_mailing_attendees(self):
if len(self) == 1:
domain = "[('event_id', '=', {})]".format(self.id)
else:
domain = "[('event_id', 'in', {})]".format(self.ids)
mass_mailing_action = dict(
name='Mass Mail Attendees',
type='ir.actions.act_window',
@@ -17,7 +21,7 @@ class Event(models.Model):
target='current',
context=dict(
default_mailing_model_id=self.env.ref('event.model_event_registration').id,
default_mailing_domain="[('event_id', 'in', %s)]" % self.ids, # , ('state', 'in', ['draft', 'open', 'done'])
default_mailing_domain=domain,
),
)
return mass_mailing_action
return mass_mailing_action
+1 -1
View File
@@ -151,7 +151,7 @@
<field name="active_move_line_ids" attrs="{'invisible': [('active_move_line_ids', '=', [])]}">
<tree editable="bottom" create="0" delete="0">
<field name="product_id"/>
<field name="product_uom_qty" readonly="1" string="To Consume"/>
<field name="product_uom_qty" readonly="1" invisible="1" string="To Consume"/>
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
<field name="qty_done"/>
<field name="product_uom_id" invisible="1"/>
@@ -7,7 +7,7 @@ from uuid import uuid4
import pprint
import logging
from odoo import _
from odoo.addons.payment.models.payment_acquirer import _partner_split_name
from odoo.exceptions import ValidationError, UserError
from odoo import _
@@ -153,6 +153,12 @@ class AuthorizeAPI():
payment_profile = etree.SubElement(profile, "paymentProfiles")
etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
billTo = etree.SubElement(payment_profile, "billTo")
if partner.is_company:
etree.SubElement(billTo, "firstName").text = ' '
etree.SubElement(billTo, "lastName").text = partner.name
else:
etree.SubElement(billTo, "firstName").text = _partner_split_name(partner.name)[0]
etree.SubElement(billTo, "lastName").text = _partner_split_name(partner.name)[1]
etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None
missing_fields = [partner._fields[field].string for field in ['city', 'country_id'] if not partner[field]]
+4 -1
View File
@@ -823,7 +823,10 @@ class PosOrder(models.Model):
'lot_id': lot_id,
})
if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding):
move.quantity_done = qty_done
if len(move._get_move_lines()) < 2:
move.quantity_done = qty_done
else:
move._set_quantity_done(qty_done)
return has_wrong_lots
def _prepare_bank_statement_line_payment_values(self, data):
@@ -1238,6 +1238,15 @@ exports.Product = Backbone.Model.extend({
var self = this;
var date = moment().startOf('day');
// In case of nested pricelists, it is necessary that all pricelists are made available in
// the POS. Display a basic alert to the user in this case.
if (pricelist === undefined) {
alert(_t(
'An error occurred when loading product prices. ' +
'Make sure all pricelists are available in the POS.'
));
}
var category_ids = [];
var category = this.categ;
while (category) {
@@ -394,7 +394,8 @@ var NumpadWidget = PosBaseWidget.extend({
this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
},
applyAccessRights: function() {
var has_price_control_rights = !this.pos.config.restrict_price_control || this.pos.get_cashier().role == 'manager';
var cashier = this.pos.get('cashier') || this.pos.get_cashier();
var has_price_control_rights = !this.pos.config.restrict_price_control || cashier.role == 'manager';
this.$el.find('.mode-button[data-mode="price"]')
.toggleClass('disabled-mode', !has_price_control_rights)
.prop('disabled', !has_price_control_rights);
@@ -1547,7 +1548,12 @@ var ReceiptScreenWidget = ScreenWidget.extend({
};
},
print_web: function() {
window.print();
if($.browser.safari){
document.execCommand('print', false, null);
}
else{
window.print();
}
this.pos.get_order()._printed = true;
},
print_xml: function() {
@@ -375,7 +375,7 @@
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="auto_printing" attrs="{'invisible': [('is_posbox', '=', False)]}">
<div class="col-xs-12 col-md-6 o_setting_box" id="auto_printing">
<div class="o_setting_left_pane">
<field name="iface_print_auto"/>
</div>
@@ -384,7 +384,7 @@
<div class="text-muted">
Print receipts automatically once the payment is registered
</div>
<div class="content-group mt16" attrs="{'invisible' : [('iface_print_auto', '=', False)]}">
<div class="content-group mt16" attrs="{'invisible' : ['|', ('iface_print_auto', '=', False), ('is_posbox', '=', False)]}">
<div>
<field name="iface_print_skip_screen" class="oe_inline"/><span class="oe_inline"><b>Skip Preview Screen</b></span>
</div>
+1 -1
View File
@@ -77,7 +77,7 @@ class CrmTeam(models.Model):
for data_point in order_data:
result.append({'x_value': data_point.get('user_id')[0], 'y_value': data_point.get('price_total')})
else:
elif self.dashboard_graph_group_pos in ['day', 'week', 'month']:
# locale en_GB is used to be able to obtain the datetime from the string returned by read_group
# /!\ do not use en_US as it's not ISO-standard and does not match datetime's library
order_data = self.env['report.pos.order'].with_context(lang='en_GB').read_group(
+10 -1
View File
@@ -103,7 +103,16 @@ class CustomerPortal(CustomerPortal):
}
# extends filterby criteria with project (criteria name is the project id)
# Note: portal users can't view projects they don't follow
projects = request.env['project.project'].sudo().search([('privacy_visibility', '=', 'portal')])
partner = request.env.user.partner_id
domain_projects = [
'&',
('privacy_visibility', '=', 'portal'),
'|',
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('task_ids.message_partner_ids', 'child_of', [partner.commercial_partner_id.id])
]
projects = request.env['project.project'].sudo().search(domain_projects)
domain = [('project_id', 'in', projects.ids)]
for proj in projects:
searchbar_filters.update({
+1 -1
View File
@@ -531,7 +531,7 @@ class Task(models.Model):
def _compute_attachment_ids(self):
for task in self:
attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids
message_attachment_ids = self.mapped('message_ids.attachment_ids').ids # from mail_thread
message_attachment_ids = task.mapped('message_ids.attachment_ids').ids # from mail_thread
task.attachment_ids = list(set(attachment_ids) - set(message_attachment_ids))
@api.multi
@@ -201,7 +201,7 @@
</strong>
<div class='row'>
<div class="col-md-2 col-sm-3 text-center oe_attachments" t-foreach='task.attachment_ids' t-as='attachment'>
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<a t-attf-href="/web/content/#{attachment.id}?download=true&amp;access_token=#{attachment.generate_access_token()}" target="_blank">
<div class='oe_attachment_embedded o_image o_image_small' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/50x40"/>
<div class='oe_attachment_name'><t t-esc='attachment.name' /></div>
</a>
@@ -124,6 +124,7 @@ class AccountInvoice(models.Model):
purchase_ids = self.invoice_line_ids.mapped('purchase_id')
if purchase_ids:
self.origin = ', '.join(purchase_ids.mapped('name'))
self.reference = ', '.join(purchase_ids.filtered('partner_ref').mapped('partner_ref')) or self.reference
@api.onchange('partner_id', 'company_id')
def _onchange_partner_id(self):
@@ -429,6 +429,18 @@ class ProductTemplate(models.Model):
('tenders', 'Propose a call for tenders')],
string='Procurement', default='rfq')
class StockMove(models.Model):
_inherit = "stock.move"
requistion_line_ids = fields.One2many('purchase.requisition.line', 'move_dest_id')
class ProcurementGroup(models.Model):
_inherit = 'procurement.group'
@api.model
def _get_exceptions_domain(self):
return super(ProcurementGroup, self)._get_exceptions_domain() + [('requistion_line_ids', '=', False)]
class ProcurementRule(models.Model):
_inherit = 'procurement.rule'
+6
View File
@@ -1195,6 +1195,12 @@ msgid "It is forbidden to modify the following fields in a locked order:\n"
"%s"
msgstr ""
#. module: sale
#: code:addons/sale/models/sale.py:551
#, python-format
msgid "It is not allowed to confirm an order in the following states: %s"
msgstr ""
#. module: sale
#: model:ir.model.fields,field_description:sale.field_report_all_channels_sales____last_update
#: model:ir.model.fields,field_description:sale.field_report_sale_report_saleproforma____last_update
+8 -1
View File
@@ -108,7 +108,14 @@ class AccountAnalyticLine(models.Model):
continue
if sale_order.state != 'sale':
raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % sale_order.name)
message_unconfirmed = _('The Sales Order %s linked to the Analytic Account %s must be validated before registering expenses.')
messages = {
'draft': message_unconfirmed,
'sent': message_unconfirmed,
'done': _('The Sales Order %s linked to the Analytic Account %s is currently locked. You cannot register an expense on a locked Sales Order. Please create a new SO linked to this Analytic Account.'),
'cancel': _('The Sales Order %s linked to the Analytic Account %s is cancelled. You cannot register an expense on a cancelled Sales Order.'),
}
raise UserError(messages[sale_order.state] % (sale_order.name, analytic_line.account_id.name))
so_line = None
price = analytic_line._sale_get_invoice_price(sale_order)
+12
View File
@@ -113,6 +113,11 @@ class SaleOrder(models.Model):
for order in self:
order.order_line._compute_tax_id()
@api.multi
def _get_payment_type(self):
self.ensure_one()
return 'form'
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)
@@ -576,11 +581,18 @@ class SaleOrder(models.Model):
@api.multi
def action_confirm(self):
if self._get_forbidden_state_confirm() & set(self.mapped('state')):
raise UserError(_(
'It is not allowed to confirm an order in the following states: %s'
) % (', '.join(self._get_forbidden_state_confirm())))
self._action_confirm()
if self.env['ir.config_parameter'].sudo().get_param('sale.auto_done_setting'):
self.action_done()
return True
def _get_forbidden_state_confirm(self):
return {'done', 'cancel'}
@api.multi
def _create_analytic_account(self, prefix=None):
for order in self:
@@ -27,6 +27,9 @@ class PaymentPortal(http.Controller):
except:
return False
# we check if the order need to create a payment token
save_token = save_token or order_sudo._get_payment_type() == "form_save"
if request.env.user == request.env.ref('base.public_user'):
save_token = False
@@ -2,10 +2,10 @@
<odoo>
<template id="report_delivery_document_inherit_sale_stock" inherit_id="stock.report_delivery_document">
<xpath expr="//div[@name='customer_address']" position="after">
<div class="row" t-if="o.sale_id.client_order_ref">
<div class="row" t-if="o.sudo().sale_id.client_order_ref">
<div class="col-xs-4 pull-right">
<strong>Customer Reference:</strong>
<p t-field="o.sale_id.client_order_ref"/>
<p t-field="o.sudo().sale_id.client_order_ref"/>
</div>
</div>
</xpath>
+11 -9
View File
@@ -132,17 +132,19 @@ class Product(models.Model):
res = dict()
for product in self.with_context(prefetch_fields=False):
res[product.id] = {}
product_id = product.id
rounding = product.uom_id.rounding
res[product_id] = {}
if dates_in_the_past:
qty_available = quants_res.get(product.id, 0.0) - moves_in_res_past.get(product.id, 0.0) + moves_out_res_past.get(product.id, 0.0)
qty_available = quants_res.get(product_id, 0.0) - moves_in_res_past.get(product_id, 0.0) + moves_out_res_past.get(product_id, 0.0)
else:
qty_available = quants_res.get(product.id, 0.0)
res[product.id]['qty_available'] = float_round(qty_available, precision_rounding=product.uom_id.rounding)
res[product.id]['incoming_qty'] = float_round(moves_in_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding)
res[product.id]['outgoing_qty'] = float_round(moves_out_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding)
res[product.id]['virtual_available'] = float_round(
qty_available + res[product.id]['incoming_qty'] - res[product.id]['outgoing_qty'],
precision_rounding=product.uom_id.rounding)
qty_available = quants_res.get(product_id, 0.0)
res[product_id]['qty_available'] = float_round(qty_available, precision_rounding=rounding)
res[product_id]['incoming_qty'] = float_round(moves_in_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['outgoing_qty'] = float_round(moves_out_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['virtual_available'] = float_round(
qty_available + res[product_id]['incoming_qty'] - res[product_id]['outgoing_qty'],
precision_rounding=rounding)
return res
+33
View File
@@ -1248,3 +1248,36 @@ class StockMove(models.Model):
return result
else:
return [(self.picking_id, self.product_id.responsible_id, visited)]
def _set_quantity_done(self, qty):
"""
Set the given quantity as quantity done on the move through the move lines. The method is
able to handle move lines with a different UoM than the move (but honestly, this would be
looking for trouble...).
@param qty: quantity in the UoM of move.product_uom
"""
for ml in self.move_line_ids:
# Convert move line qty into move uom
ml_qty = ml.product_uom_qty - ml.qty_done
if ml.product_uom_id != self.product_uom:
ml_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)
taken_qty = min(qty, ml_qty)
# Convert taken qty into move line uom
if ml.product_uom_id != self.product_uom:
taken_qty = self.product_uom._compute_quantity(ml_qty, ml.product_uom_id, round=False)
# Assign qty_done and explicitly round to make sure there is no inconsistency between
# ml.qty_done and qty.
taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding)
ml.qty_done += taken_qty
if ml.product_uom_id != self.product_uom:
taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)
qty -= taken_qty
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0:
break
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) > 0:
vals = self._prepare_move_line_vals(quantity=0)
vals['qty_done'] = qty
ml = self.env['stock.move.line'].create(vals)
+9 -1
View File
@@ -59,7 +59,15 @@
<tbody>
<t t-set="lines" t-value="o.move_lines.filtered(lambda x: x.product_uom_qty)"/>
<tr t-foreach="lines" t-as="move">
<td><span t-field="move.product_id"/></td>
<td>
<span t-field="move.product_id"/>
<p t-if="o.picking_type_code == 'outgoing'">
<span t-field="move.product_id.sudo().description_pickingout"/>
</p>
<p t-if="o.picking_type_code == 'incoming'">
<span t-field="move.product_id.sudo().description_pickingin"/>
</p>
</td>
<td>
<span t-field="move.product_uom_qty"/>
<span t-field="move.product_uom"/>
@@ -73,7 +73,7 @@
<field name="model">product.product</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="display_name"/>
<field name="qty_at_date"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="currency_id" invisible="1"/>
+1 -1
View File
@@ -86,6 +86,6 @@ class IrTranslation(models.Model):
'module': translation.module,
'src': werkzeug.url_quote_plus(
"text:'" +
translation.source[:50].replace("'", "\'") +
translation.source[:50].replace("'", "\\'") +
"'"),
}
+1 -1
View File
@@ -1290,7 +1290,7 @@ class Export(http.Controller):
'relation_field': field.get('relation_field')}
records.append(record)
if len(name.split('/')) < 3 and 'relation' in field:
if len(id.split('/')) < 3 and 'relation' in field:
ref = field.pop('relation')
record['value'] += '/id'
record['params'] = {'model': ref, 'prefix': id, 'name': name}
+6 -4
View File
@@ -642,13 +642,15 @@ var relativedelta = py.type('relativedelta', null, {
this._fix();
},
_fix: function () {
var self = this;
var months = asJS(this.ops.months);
if (Math.abs(months) > 11) {
var s = months > 0 ? 1 : -1;
var r = utils.divmod(months * s, 12);
this.ops.months = py.float.fromJSON(r.mod*s);
this.ops.years = py.float.fromJSON(
asJS(this.ops.years) + r.div*s);
utils.divmod(months * s, 12, function (years, months) {
self.ops.months = py.float.fromJSON(months*s);
self.ops.years = py.float.fromJSON(
asJS(self.ops.years) + years*s);
});
}
this._has_time = 0;
},
@@ -447,7 +447,7 @@ var FieldDate = InputField.extend({
this.datewidget = this._makeDatePicker();
this.datewidget.on('datetime_changed', this, function () {
var value = this._getValue();
if ((!value && this.value) || (value && !value.isSame(this.value))) {
if ((!value && this.value) || (value && !this._isSameValue(value))) {
this._setValue(value);
}
});
@@ -2348,19 +2348,18 @@ var FieldDomain = AbstractField.extend({
// Convert char value to array value
var value = this.value || "[]";
var domain = Domain.prototype.stringToArray(value);
// Create the domain selector or change the value of the current one...
var def;
if (!this.domainSelector) {
this.domainSelector = new DomainSelector(this, this._domainModel, domain, {
this.domainSelector = new DomainSelector(this, this._domainModel, value, {
readonly: this.mode === "readonly" || this.inDialog,
filters: this.fsFilters,
debugMode: session.debug,
});
def = this.domainSelector.prependTo(this.$el);
} else {
def = this.domainSelector.setDomain(domain);
def = this.domainSelector.setDomain(value);
}
// ... then replace the other content (matched records, etc)
return def.then(this._replaceContent.bind(this));
@@ -578,10 +578,11 @@ var FieldMany2One = AbstractField.extend({
* @param {OdooEvent} ev
*/
_onInputKeyup: function (ev) {
if (ev.which === $.ui.keyCode.ENTER) {
// If we pressed enter, we want to prevent _onInputFocusout from
if (ev.which === $.ui.keyCode.ENTER || ev.which === $.ui.keyCode.TAB) {
// If we pressed enter or tab, we want to prevent _onInputFocusout from
// executing since it would open a M2O dialog to request
// confirmation that the many2one is not properly set.
// It's a case that is already handled by the autocomplete lib.
return;
}
this.isDirty = true;
@@ -2447,11 +2447,11 @@ var BasicModel = AbstractModel.extend({
}
var def = $.Deferred();
var evalContext = this._getEvalContext(record);
this._rpc({
model: domainModel,
method: 'search_count',
args: [Domain.prototype.stringToArray(domainValue)],
args: [Domain.prototype.stringToArray(domainValue, evalContext)],
context: context
})
.then(_.identity, function (error, e) {
@@ -61,11 +61,15 @@ var KanbanModel = BasicModel.extend({
if (!groupByField || groupByField.type !== 'many2one') {
return $.Deferred().reject(); // only supported when grouped on m2o
}
// ignore action default which may be for another model
var context = _.pick(parent.context, function (value, key) {
return key.slice(0,8) !== 'default_';
});
return this._rpc({
model: groupByField.relation,
method: 'name_create',
args: [name],
context: parent.context, // todo: combine with view context
context: context, // todo: combine with view context
})
.then(function (result) {
var newGroup = self._makeDataPoint({
@@ -272,6 +276,9 @@ var KanbanModel = BasicModel.extend({
options.groupBy = this.defaultGroupedBy;
}
var def = this._super(id, options);
if (options && options.loadMoreOffset) {
return def;
}
return this._reloadProgressBarGroupFromRecord(id, def);
},
/**
@@ -193,7 +193,6 @@ var ListRenderer = BasicRenderer.extend({
reject = columnInvisibleFields[c.attrs.name];
}
if (!reject && c.attrs.widget === 'handle') {
self.hasHandle = true;
self.handleField = c.attrs.name;
}
return reject;
@@ -657,6 +656,8 @@ var ListRenderer = BasicRenderer.extend({
this._computeAggregates();
$table.toggleClass('o_list_view_grouped', is_grouped);
$table.toggleClass('o_list_view_ungrouped', !is_grouped);
this.hasHandle = this.state.orderedBy.length === 0 ||
this.state.orderedBy[0].name === this.handleField;
if (is_grouped) {
$table
.append(this._renderHeader(true))
@@ -14,6 +14,16 @@ var PivotRenderer = AbstractRenderer.extend({
'hover td': '_onTdHover',
}),
/**
* @override
* @param {Object} [params.widgets] mapping (fieldName -> widget) used to
* format the cells
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.fieldWidgets = params.widgets || {};
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -141,7 +151,8 @@ var PivotRenderer = AbstractRenderer.extend({
if (value !== undefined) {
name = this.state.measures[j % nbrMeasures];
measure = this.state.fields[name];
value = field_utils.format[measureTypes[j % nbrMeasures]](value, measure);
var formatter = this.fieldWidgets[name] || measureTypes[j % nbrMeasures];
value = field_utils.format[formatter](value, measure);
}
$cell = $('<td>')
.data('id', rows[i].id)
@@ -44,6 +44,7 @@ var PivotView = AbstractView.extend({
var measures = {};
var groupableFields = {};
var widgets = {};
this.fields.__count = {string: _t("Count"), type: "integer"};
_.each(this.fields, function (field, name) {
@@ -65,6 +66,10 @@ var PivotView = AbstractView.extend({
name += ':' + field.attrs.interval;
}
if (field.attrs.widget) {
widgets[name] = field.attrs.widget;
}
// add active measures to the measure list. This is very rarely
// necessary, but it can be useful if one is working with a
// functional field non stored, but in a model with an overrided
@@ -96,6 +101,8 @@ var PivotView = AbstractView.extend({
this.loadParams.rowGroupBys = rowGroupBys;
this.loadParams.fields = this.fields;
this.rendererParams.widgets = widgets;
this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled");
this.controllerParams.enableLinking = !this.arch.attrs.disable_linking;
this.controllerParams.measures = measures;
@@ -177,18 +177,12 @@ var DomainTree = DomainNode.extend({
* operator from the domain.
* @see DomainTree._addFlattenedChildren
*/
init: function (parent, model, domain, options) {
init: function (parent, model, domain) {
this._super.apply(this, arguments);
try {
domain = Domain.prototype.stringToArray(domain);
} catch (err) {
// TODO: domain could contain `parent` for example, which is
// currently not handled by the DomainSelector
this.invalidDomain = true;
this.children = [];
return;
var parsedDomain = this._parseDomain(domain);
if (parsedDomain) {
this._initialize(parsedDomain);
}
this._initialize(domain);
},
/**
* @see DomainNode.start
@@ -386,6 +380,23 @@ var DomainTree = DomainNode.extend({
});
}).bind(this));
},
/**
* @param {string} domain
* @returns {Array[]}
*/
_parseDomain: function (domain) {
var parsedDomain = false;
try {
parsedDomain = Domain.prototype.stringToArray(domain);
this.invalidDomain = false;
} catch (err) {
// TODO: domain could contain `parent` for example, which is
// currently not handled by the DomainSelector
this.invalidDomain = true;
this.children = [];
}
return parsedDomain;
},
//--------------------------------------------------------------------------
// Handlers
@@ -471,14 +482,17 @@ var DomainSelector = DomainTree.extend({
* If the internal domain value was already equal to the given one, this
* does nothing.
*
* @param {Array|string} domain
* @param {string} domain
* @returns {Deferred} resolved when the rerendering is finished
*/
setDomain: function (domain) {
if (Domain.prototype.arrayToString(domain) === Domain.prototype.arrayToString(this.getDomain())) {
if (domain === Domain.prototype.arrayToString(this.getDomain())) {
return $.when();
}
return this._redraw(domain);
var parsedDomain = this._parseDomain(domain);
if (parsedDomain) {
return this._redraw(parsedDomain);
}
},
//--------------------------------------------------------------------------
+9 -1
View File
@@ -319,7 +319,7 @@ QUnit.module('core', function () {
});
QUnit.test('relativedelta', function (assert) {
assert.expect(5);
assert.expect(7);
assert.strictEqual(
py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')",
@@ -341,6 +341,14 @@ QUnit.module('core', function () {
py.eval("(datetime.date(2015,2,5)+relativedelta(days=-6,weekday=0)).strftime('%Y-%m-%d')",
pyEval.context()),
'2015-02-02');
assert.strictEqual(
py.eval("(datetime.date(2018, 2, 1) + relativedelta(years=7, months=42, days=42)).strftime('%Y-%m-%d')",
pyEval.context()),
'2028-09-12');
assert.strictEqual(
py.eval("(datetime.date(2018, 2, 1) + relativedelta(years=-7, months=-42, days=-42)).strftime('%Y-%m-%d')",
pyEval.context()),
'2007-06-20');
});
@@ -4418,6 +4418,33 @@ QUnit.module('basic_fields', {
QUnit.module('FieldDomain');
QUnit.test('The domain editor should not crash the view when given a dynamic filter', function (assert) {
//dynamic filters (containing variables, such as uid, parent or today)
//are not handled by the domain editor, but it shouldn't crash the view
assert.expect(1);
this.data.partner.records[0].foo = '[["int_field", "=", uid]]';
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' +
'<field name="int_field" invisible="1"/>' +
'</form>',
res_id: 1,
session: {
user_context: {uid: 14},
},
});
assert.strictEqual(form.$('.o_read_mode').text(), "This domain is not supported.",
"The widget should not crash the view, but gracefully admit its failure.");
form.destroy();
});
QUnit.test('basic domain field usage is ok', function (assert) {
assert.expect(6);
@@ -4567,6 +4594,43 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('domain field can be reset with a new domain (from onchange)', function (assert) {
assert.expect(2);
this.data.partner.records[0].foo = '[]';
this.data.partner.onchanges = {
display_name: function (obj) {
obj.foo = '[["id", "=", 1]]';
},
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<field name="display_name"/>' +
'<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' +
'</form>',
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.equal(form.$('.o_domain_show_selection_button').text().trim(), '5 record(s)',
"the domain being empty, there should be 5 records");
// update display_name to trigger the onchange and reset foo
form.$('.o_field_widget[name=display_name]').val('new value').trigger('input');
assert.equal(form.$('.o_domain_show_selection_button').text().trim(), '1 record(s)',
"the domain has changed, there should be only 1 record");
form.destroy();
});
QUnit.test('domain field: handle false domain as []', function (assert) {
assert.expect(3);
@@ -2226,7 +2226,7 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should not trigger M2ODialog', function (assert) {
QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should open a M2ODialog', function (assert) {
var done = assert.async();
assert.expect(2);
@@ -2277,6 +2277,68 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('select a value by pressing TAB on a many2one with onchange', function (assert) {
var done = assert.async();
assert.expect(3);
this.data.partner.onchanges.trululu = function () {};
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
var def = $.Deferred();
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="trululu"/>' +
'<field name="display_name"/>' +
'</form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
return $.when(def).then(_.constant(result));
}
return result;
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
var $input = form.$('.o_field_many2one input');
$input.val("first").trigger('input');
concurrency.delay(0).then(function () {
var evOptions = {
which: $.ui.keyCode.TAB,
keyCode: $.ui.keyCode.TAB,
};
$input.trigger($.Event('keydown', evOptions));
$input.trigger($.Event('keypress', evOptions));
$input.trigger($.Event('keyup', evOptions));
// simulate a focusout (e.g. because the user clicks outside)
// before the onchange returns
form.$('.o_field_char').focus();
assert.strictEqual($('.modal').length, 0,
"there shouldn't be any modal in body");
// unlock the onchange
def.resolve();
assert.strictEqual($input.val(), 'first record',
"first record should have been selected");
assert.strictEqual($('.modal').length, 0,
"there shouldn't be any modal in body");
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
form.destroy();
done();
});
});
QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', function (assert) {
assert.expect(6);
var done = assert.async();
@@ -890,7 +890,7 @@ var MockServer = Class.extend({
var field = args.field ? args.field : 'sequence';
var records = this.data[args.model].records;
for (var i in args.ids) {
var record = _.findWhere(this.data[args.model].records, {id: args.ids[i]});
var record = _.findWhere(records, {id: args.ids[i]});
record[field] = Number(i) + offset;
}
},
@@ -1157,6 +1157,43 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('m2o group record created does not use action defaults', function (assert) {
assert.expect(1);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
'<templates><t t-name="kanban-box">' +
'<div>' +
'<field name="foo"/>' +
'</div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
mockRPC: function (route, args) {
if (args.method === 'name_create') {
assert.notOk(_.has(args.kwargs.context, 'default_name'),
"default_* should be removed from context");
}
return this._super.apply(this, arguments);
},
viewOptions: {
context: {
default_name: 'default name partner',
},
},
});
kanban.renderButtons();
kanban.$('.o_column_quick_create input').val('new product');
kanban.$('.o_column_quick_create button.o_kanban_add').click();
kanban.destroy();
});
QUnit.test('quick create record fail in grouped', function (assert) {
assert.expect(7);
@@ -3273,6 +3310,37 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('column progressbars are working with load more', function (assert) {
assert.expect(1);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
domain: [['bar', '=', true]],
arch:
'<kanban limit="1">' +
'<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\'/>' +
'<templates><t t-name="kanban-box">' +
'<div>' +
'<field name="id"/>' +
'</div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['bar'],
});
// we have 1 record shown, load 2 more and check it worked
kanban.$('.o_kanban_group').find('.o_kanban_load_more').click();
kanban.$('.o_kanban_group').find('.o_kanban_load_more').click();
var shownIDs = _.map(kanban.$('.o_kanban_record'), function(record) {
return parseInt(record.innerText);
});
assert.deepEqual(shownIDs, [1, 2, 3], "intended records are loaded");
kanban.destroy();
});
// XXX test deactivated in saas~11.2 as kanban archiving has been removed (and readded in saas~11.4)
QUnit.skip('column progressbars on archiving records update counter', function (assert) {
assert.expect(4);
@@ -3235,6 +3235,76 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) {
assert.expect(6);
// we need another sortable field to lock/unlock the handle
this.data.foo.fields.amount.sortable = true;
// resequence makes sense on a sequence field, not on arbitrary fields
this.data.foo.records[0].int_field = 0;
this.data.foo.records[1].int_field = 1;
this.data.foo.records[2].int_field = 2;
this.data.foo.records[3].int_field = 3;
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top" default_order="int_field">' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float"/>' +
'</tree>',
});
assert.strictEqual(list.$('tbody').text(), '1200.00500.00300.000.00',
"default should be sorted by int_field");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
// Handle should be unlocked at this point
assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00',
"drag and drop should have succeeded, as the handle is unlocked");
// Sorting by a field different for int_field should lock the handle
list.$('.o_column_sortable').eq(1).click();
assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00',
"should have been sorted by amount");
// Drag and drop the fourth line in second position (not)
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00',
"drag and drop should have failed as the handle is locked");
// Sorting by int_field should unlock the handle
list.$('.o_column_sortable').eq(0).click();
assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00',
"records should be ordered as per the previous resequence");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
assert.strictEqual(list.$('tbody').text(), '1200.00300.000.00500.00',
"drag and drop should have worked as the handle is unlocked");
list.destroy();
});
QUnit.test('editable list with handle widget with slow network', function (assert) {
assert.expect(15);
@@ -110,6 +110,23 @@ QUnit.module('Views', {
pivot.destroy();
});
QUnit.test('pivot rendering with widget', function (assert) {
assert.expect(1);
var pivot = createView({
View: PivotView,
model: "partner",
data: this.data,
arch: '<pivot string="Partners">' +
'<field name="foo" type="measure" widget="float_time"/>' +
'</pivot>',
});
assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(32:00)').length, 1,
"should contain a pivot cell with the sum of all records");
pivot.destroy();
});
QUnit.test('pivot view without "string" attribute', function (assert) {
assert.expect(1);
@@ -207,11 +207,15 @@ define([
var layoutInfo = dom.makeLayoutInfo(target);
/* ODOO: (start_modification */
var $editable = layoutInfo.editable();
if (!event.isDefaultPrevented()) {
modules.editor.saveRange($editable);
if (event.setStyleInfoFromEditable) {
var styleInfo = modules.editor.styleFromNode($editable);
} else {
if (!event.isDefaultPrevented()) {
modules.editor.saveRange($editable);
}
var styleInfo = modules.editor.currentStyle(target);
}
/* ODOO: end_modification) */
var styleInfo = modules.editor.currentStyle(target);
self.updateStyleInfo(styleInfo, layoutInfo);
}, 0);
};
@@ -225,7 +225,8 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
this.$content = this.$('.note-editable:first');
this.$content.html(this._textToHtml(this.value));
// trigger a mouseup to refresh the editor toolbar
this.$content.trigger('mouseup');
var mouseupEvent = $.Event('mouseup', {'setStyleInfoFromEditable': true});
this.$content.trigger(mouseupEvent);
if (this.nodeOptions['style-inline']) {
transcoder.styleToClass(this.$content);
transcoder.imgToFont(this.$content);
@@ -157,7 +157,7 @@ var History = function History($editable) {
if (aUndo[pos]) {
pos = Math.min(pos, aUndo.length);
aUndo.splice(Math.max(pos,1), aUndo.length);
aUndo.splice(pos, aUndo.length);
}
// => make a snap when the user change editable zone (because: don't make snap for each keydown)
@@ -1077,7 +1077,14 @@ $.summernote.pluginEvents.untab = function (event, editor, layoutInfo) {
$.summernote.pluginEvents.up = function (event, editor, layoutInfo) {
var r = range.create();
var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
if (!r.isOnCell() || (!dom.isCell(node) && dom.hasContentBefore(node) && (!dom.isBR(dom.hasContentBefore(node)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(node))))) {
if (!r.isOnCell()) {
return;
}
// check if an ancestor between node and cell has content before
var ancestor = dom.ancestor(node, function (ancestorNode) {
return dom.hasContentBefore(ancestorNode) || dom.isCell(ancestorNode);
});
if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentBefore(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(ancestor)))) {
return;
}
event.preventDefault();
@@ -1094,7 +1101,14 @@ $.summernote.pluginEvents.up = function (event, editor, layoutInfo) {
$.summernote.pluginEvents.down = function (event, editor, layoutInfo) {
var r = range.create();
var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
if (!r.isOnCell() || (!dom.isCell(node) && dom.hasContentAfter(node) && (!dom.isBR(dom.hasContentAfter(node)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(node))))) {
if (!r.isOnCell()) {
return;
}
// check if an ancestor between node and cell has content after
var ancestor = dom.ancestor(node, function (ancestorNode) {
return dom.hasContentAfter(ancestorNode) || dom.isCell(ancestorNode);
});
if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentAfter(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(ancestor)))) {
return;
}
event.preventDefault();
@@ -516,7 +516,7 @@ var ImageWidget = MediaWidget.extend({
self.$('.well > span').remove();
self.$('.well > div').show();
_.each(attachments, function (record) {
record.src = record.url || '/web/image/' + record.id;
record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, encodeURI(record.name)); // Name is added for SEO purposes
record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype));
});
if (error || !attachments.length) {
+3 -3
View File
@@ -767,11 +767,11 @@ class Page(models.Model):
for page in self:
# Other pages linked to the ir_ui_view of the page being deleted (will it even be possible?)
pages_linked_to_iruiview = self.search(
[('view_id', '=', self.view_id.id), ('id', '!=', self.id)]
[('view_id', '=', page.view_id.id), ('id', '!=', page.id)]
)
if len(pages_linked_to_iruiview) == 0:
if not pages_linked_to_iruiview:
# If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view
self.env['ir.ui.view'].search([('id', '=', self.view_id.id)]).unlink()
page.view_id.unlink()
# And then delete the website_page itself
return super(Page, self).unlink()
+2 -2
View File
@@ -47,12 +47,12 @@
<button name="action_open_badge_editor"
type="object"
states="confirm"
string="Edit Badges"
string="Preview Badges"
class="oe_highlight"/>
<button name="action_open_badge_editor"
type="object"
states="done"
string="Edit Badges"/>
string="Preview Badges"/>
</xpath>
</field>
</record>
+1
View File
@@ -105,6 +105,7 @@ class SaleOrder(models.Model):
'order_id': order_id,
'product_uom': product.uom_id.id,
'price_unit': pu,
'customer_lead': product.sale_delay,
}
@api.multi
+15
View File
@@ -0,0 +1,15 @@
Canada, August 27th, 2018
D2GO SOLUTIONS INC 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,
Louis-Philippe Papillon lppapillon@attrix.ca https://github.com/stechnique
List of contributors:
Louis-Philippe Papillon lppapillon@attrix.ca https://github.com/stechnique
+11
View File
@@ -0,0 +1,11 @@
Mauritius, 2018-08-27
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,
Abdur-Rahmaan Janhangeer cryptolabour@gmail.com https://github.com/Abdur-RahmaanJ
+11
View File
@@ -0,0 +1,11 @@
India, 2018-08-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,
Devendra kavthekar dkatodoo@gmail.com https://github.com/dek-odoo
+1 -1
View File
@@ -1760,7 +1760,7 @@ Examples can be easily adapted from XML-RPC to JSON-RPC.
systems without *explicitly* going through XML-RPC or JSON-RPC, such as:
* https://github.com/akretion/ooor
* https://github.com/syleam/openobject-library
* https://github.com/OCA/odoorpc
* https://github.com/nicolas-van/openerp-client-lib
* http://pythonhosted.org/OdooRPC
* https://github.com/abhishek-jaiswal/php-openerp-lib
+1 -1
View File
@@ -446,7 +446,7 @@ of them in the new class.
},
};
var Hamster = Hamster.extend(DanceMixin, {
var Hamster = Animal.extend(DanceMixin, {
sleep: function () {
console.log('sleeping');
},
+2 -2
View File
@@ -499,8 +499,8 @@ necessary to clear caches when using ``CREATE``, ``UPDATE`` or ``DELETE`` in
SQL, but not ``SELECT`` (which simply reads the database).
Clearing caches can be performed using the
:meth:`~odoo.api.Environment.invalidate_all` method of the
:class:`~odoo.api.Environment` object.
:meth:`~odoo.models.BaseModel.invalidate_cache` method of the
:class:`~odoo.models.BaseModel` object.
.. _reference/orm/oldapi:
@@ -1787,6 +1787,13 @@
<field name="currency_id" ref="ZWD" />
<field eval="263" name="phone_code" />
</record>
<record id="xk" model="res.country">
<field name="name">Kosovo</field>
<field name="code">xk</field>
<field file="base/static/img/country_flags/xk.png" name="image" type="base64" />
<field name="currency_id" ref="EUR" />
<field eval="383" name="phone_code" />
</record>
<record id="europe" model="res.country.group">
<field name="name">Europe</field>
+1
View File
@@ -394,6 +394,7 @@ class AssetsBundle(object):
if not self.css_errors and old_attachments:
old_attachments.unlink()
old_attachments = None
fragments = self.rx_css_split.split(compiled)
at_rules = fragments.pop(0)
+5
View File
@@ -350,6 +350,11 @@ class IrAttachment(models.Model):
if res_model not in self.env:
require_employee = True
continue
elif res_model == 'res.users' and len(res_ids) == 1 and self._uid == list(res_ids)[0]:
# by default a user cannot write on itself, despite the list of writeable fields
# e.g. in the case of a user inserting an image into his image signature
# we need to bypass this check which would needlessly throw us away
continue
records = self.env[res_model].browse(res_ids).exists()
if len(records) < len(res_ids):
require_employee = True
+6
View File
@@ -928,6 +928,12 @@ actual arch.
'datetime',
'relativedelta',
'current_date',
'abs',
'len',
'bool',
'float',
'str',
'unicode',
}
def get_attrs_field_names(self, arch, model, editable):
+2 -1
View File
@@ -12,6 +12,7 @@ from textwrap import dedent
import itertools
from lxml import etree, html
from psycopg2.extensions import TransactionRollbackError
import werkzeug
from werkzeug.utils import escape as _escape
@@ -340,7 +341,7 @@ class QWeb(object):
new.update(values)
try:
return compiled(self, append, new, options, log)
except QWebException as e:
except (QWebException, TransactionRollbackError) as e:
raise e
except Exception as e:
path = log['last_path_node']
+1 -1
View File
@@ -575,7 +575,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
IrDefault.set(model, field, value)
# group fields: modify group / implied groups
current_settings = self.new(self.default_get(list(self.fields_get())))
current_settings = self.default_get(list(self.fields_get()))
with self.env.norecompute():
for name, groups, implied_group in classified['group']:
if self[name] == current_settings[name]:
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+1 -1
View File
@@ -72,7 +72,7 @@
<group>
<field name="bank_id"/>
<field name="currency_id" groups="base.group_multi_currency" options="{'no_create': True}"/>
<field name="acc_holder_name" groups="base.group_no_one"/>
<field name="acc_holder_name"/>
</group>
</group>
</form>
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import contextlib
import io
import tempfile
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
@@ -30,7 +29,7 @@ class BaseUpdateTranslations(models.TransientModel):
def act_update(self):
this = self[0]
lang_name = self._get_lang_name(this.lang)
with contextlib.closing(io.BytesIO()) as buf:
tools.trans_export(this.lang, ['all'], buf, 'csv', self._cr)
tools.trans_load_data(self._cr, buf, 'csv', this.lang, lang_name=lang_name)
with tempfile.NamedTemporaryFile() as buf:
tools.trans_export(this.lang, ['all'], buf, 'po', self._cr)
tools.trans_load_data(self._cr, buf, 'po', this.lang, lang_name=lang_name)
return {'type': 'ir.actions.act_window_close'}
@@ -14,11 +14,11 @@ msgstr ""
"Plural-Forms: \n"
# Note: there is normally an additional line:
# #: code:addons/test_translation_import/models.py:17
# #: code:addons/test_translation_import/models.py:15
# This line is present in the POT and removed here to test the translation
# import behavior.
#. module: test_translation_import
#: field:test.translation.import,name:0
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__name
#, python-format
msgid "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB"
msgstr "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB in french"
@@ -50,3 +50,20 @@ msgstr "Test translation import in french"
msgid "Test translation"
msgstr "Test translation in french"
#. module: test_translation_import
#: code:addons/base/models/arthur.py:42
#: code:addons/base/models/arthur.py:43
msgid "Test translation with two code lines"
msgstr "Test traduction avec deux lignes de code"
#. module: test_translation_import
#: code:addons/base/models/ford.py:43
msgid "Test translation with a code type but different line number in pot"
msgstr "Test traduction avec un type code mais différent numéro de ligne dans le pot"
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__other_name
#: code:addons/base/models/marvin.py:42
#: code:addons/base/models/trillian.py:42
msgid "Test translation with two code type and model"
msgstr "Test traduction avec deux type code et un model"
@@ -23,8 +23,8 @@ msgstr ""
"Plural-Forms: \n"
#. module: test_translation_import
#: code:addons/test_translation_import/models.py:17
#: field:test.translation.import,name:0
#: code:addons/test_translation_import/models.py:15
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__name
#, python-format
msgid "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB"
msgstr ""
@@ -56,3 +56,20 @@ msgstr ""
msgid "Test translation"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/arthur.py:42
#: code:addons/base/models/arthur.py:43
msgid "Test translation with two code lines"
msgstr ""
#. module: test_translation_import
#: code:addons/base/models/ford.py:42
msgid "Test translation with a code type but different line number in pot"
msgstr ""
#. module: test_translation_import
#: model:ir.model.fields,field_description:test_translation_import.field_test_translation_import__other_name
#: code:addons/base/models/marvin.py:42
#: code:addons/base/models/trillian.py:42
msgid "Test translation with two code type and model"
msgstr ""
@@ -8,6 +8,7 @@ class m(models.TransientModel):
name = fields.Char('1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB',
size=32, help='Efgh')
other_name = fields.Char('Test translation with two code type and model')
_('Ijkl')
@@ -1,15 +1,11 @@
# -*- coding: utf-8 -*-
from contextlib import closing
import io
import odoo
from odoo.tests import common
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
class TestTermCount(common.TransactionCase):
@@ -17,7 +13,7 @@ class TestTermCount(common.TransactionCase):
"""
Just make sure we have as many translation entries as we wanted.
"""
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False)
ids = self.env['ir.translation'].search(
[('src', '=', '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB')])
self.assertEqual(len(ids), 2)
@@ -29,21 +25,39 @@ class TestTermCount(common.TransactionCase):
menu = self.env.ref('test_translation_import.menu_test_translation_import')
menu.name = "New Name"
# install french and change translation content
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False)
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False)
menu.with_context(lang='fr_FR').name = "Nouveau nom"
# reload with overwrite
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', verbose=False, context={'overwrite': True})
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False, context={'overwrite': True})
# trans_load invalidates ormcache but not record cache
menu.refresh()
menu.clear_caches()
self.assertEqual(menu.name, "New Name")
self.assertEqual(menu.with_context(lang='fr_FR').name, "Nouveau nom")
def test_no_duplicate(self):
"""
Just make sure we do not create duplicated translation with 'code' type
"""
odoo.tools.trans_load(self.cr, 'test_translation_import/i18n/fr.po', 'fr_FR', module_name='test_translation_import', verbose=False)
ids = self.env['ir.translation'].search(
[('src', '=', 'Test translation with two code lines')])
self.assertEqual(len(ids), 1)
ids = self.env['ir.translation'].search(
[('src', '=', 'Test translation with a code type but different line number in pot')])
self.assertEqual(len(ids), 1)
ids = self.env['ir.translation'].search(
[('src', '=', 'Test translation with two code type and model')])
self.assertEqual(len(ids), 2)
self.assertEqual(len(ids.filtered(lambda t: t.type == 'code')), 1)
def test_export_empty_string(self):
"""When the string and the translation is equal the translation is empty"""
# Export the translations
def update_translations():
with closing(StringIO()) as bufferobj:
with closing(io.BytesIO()) as bufferobj:
odoo.tools.trans_export('fr_FR', ['test_translation_import'], bufferobj, 'po', self.cr)
bufferobj.name = 'test_translation_import/i18n/fr.po'
odoo.tools.trans_load_data(self.cr, bufferobj, 'po', 'fr_FR', verbose=False, context={'overwrite': True})
+3 -11
View File
@@ -55,7 +55,7 @@ from .tools import frozendict, lazy_classproperty, lazy_property, ormcache, \
Collector, LastOrderedSet, OrderedSet, pycompat, groupby
from .tools.config import config
from .tools.func import frame_codeinfo
from .tools.misc import CountingStream, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from .tools.misc import CountingStream, clean_context, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from .tools.safe_eval import safe_eval
from .tools.translate import _
@@ -3246,11 +3246,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# set the value of non-column fields
if other_fields:
# discard default values from context
other = self.with_context({
key: val
for key, val in self._context.items()
if not key.startswith('default_')
})
other = self.with_context(clean_context(self._context))
for field in sorted(other_fields, key=attrgetter('_sequence')):
field.write(other, vals[field.name])
@@ -3419,11 +3415,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# set the value of non-column fields
if other_fields:
# discard default values from context
other = self.with_context({
key: val
for key, val in self._context.items()
if not key.startswith('default_')
})
other = self.with_context(clean_context(self._context))
for field in sorted(other_fields, key=attrgetter('_sequence')):
field.write(other, vals[field.name], create=True)
+1 -1
View File
@@ -119,7 +119,7 @@ def init_logger():
if dirname and not os.path.isdir(dirname):
os.makedirs(dirname)
if tools.config['logrotate'] is not False:
if tools.config['workers'] > 1:
if tools.config['workers'] and tools.config['workers'] > 1:
# TODO: fallback to regular file logging in master for safe(r) defaults?
#
# Doing so here would be a good idea but also might break
+2
View File
@@ -201,6 +201,8 @@ def crop_image(data, type='top', ratio=False, size=None, image_format="PNG"):
raise ValueError('ERROR: invalid value for crop_type')
if size:
thumbnail = Image.open(io.BytesIO(output_stream.getvalue()))
output_stream.truncate(0)
output_stream.seek(0)
thumbnail.thumbnail(size, Image.ANTIALIAS)
thumbnail.save(output_stream, image_format)
return base64.b64encode(output_stream.getvalue())
+4
View File
@@ -960,6 +960,10 @@ def freehash(arg):
else:
return id(arg)
def clean_context(context):
""" This function take a dictionary and remove each entry with its key starting with 'default_' """
return {k: v for k, v in context.items() if not k.startswith('default_')}
class frozendict(dict):
""" An implementation of an immutable dictionary. """
def __delitem__(self, key):
+7 -4
View File
@@ -584,8 +584,11 @@ class PoFile(object):
# end of this next() call), and keep the others to generate
# additional entries (returned the next next() calls).
trans_type, name, res_id = targets.pop(0)
code = trans_type == 'code'
for t, n, r in targets:
if t == trans_type == 'code': continue
if t == 'code' and code: continue
if t == 'code':
code = True
self.extra_lines.append((t, n, r, source, trad, comments))
if name is None:
@@ -1080,7 +1083,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
for type, name, res_id, src, _ignored, comments in pot_reader:
if type is not None:
target = pot_targets[src]
target.targets.add((type, name, res_id))
target.targets.add((type, name, type != 'code' and res_id or 0))
target.comments = comments
# read the rest of the file
@@ -1105,11 +1108,11 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
return
target.value = dic['value']
target.targets.discard((dic['type'], dic['name'], dic['res_id']))
target.targets.discard((dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0))
# This would skip terms that fail to specify a res_id
res_id = dic['res_id']
if not res_id:
if not res_id and dic['type'] != 'code':
return
if isinstance(res_id, pycompat.integer_types) or \