[MERGE] forward port branch saas-11.3 up to 4850fb0838
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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')
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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&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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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("'", "\\'") +
|
||||
"'"),
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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 |
@@ -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'}
|
||||
|
||||
+19
-2
@@ -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"
|
||||
+19
-2
@@ -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 ""
|
||||
+1
@@ -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')
|
||||
|
||||
+24
-10
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user