diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index e5c3e0ef99b..9c43cf5ea50 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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() diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 791d0820f63..fb4c2598e7e 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -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 diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 286392c43ed..5399633367b 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -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), diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 6c554ec3dce..c239de5515e 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -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 diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index dafb0e484b4..2644163e5e3 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -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) diff --git a/addons/delivery/models/delivery_grid.py b/addons/delivery/models/delivery_grid.py index 4fe4037aa5a..d74f3014898 100644 --- a/addons/delivery/models/delivery_grid.py +++ b/addons/delivery/models/delivery_grid.py @@ -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: diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js index 4a19b2b9b28..2e87833bfa1 100644 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -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)); diff --git a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py index f271da9dc84..2e5ae0eacdb 100644 --- a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py +++ b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py @@ -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): diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index cbacf5c70d5..c526d629ef9 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -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 # ------------------------------------------------------------ diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 9013dc2f518..8ff2d5d6d41 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -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 # ------------------------------------------------------------ diff --git a/addons/hr_holidays/report/hr_leave_report.py b/addons/hr_holidays/report/hr_leave_report.py index 3e918920c52..50f4c1a69ca 100644 --- a/addons/hr_holidays/report/hr_leave_report.py +++ b/addons/hr_holidays/report/hr_leave_report.py @@ -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 diff --git a/addons/hr_holidays/security/hr_holidays_security.xml b/addons/hr_holidays/security/hr_holidays_security.xml index 5b5c4eeb8da..7e5f51aa67b 100644 --- a/addons/hr_holidays/security/hr_holidays_security.xml +++ b/addons/hr_holidays/security/hr_holidays_security.xml @@ -18,64 +18,96 @@ - Employee Leaves + Leaves: employee: read all - [('employee_id.user_id','=',user.id)] + [(1, '=', 1)] - - Employee Leaves Create, Write, Unlink + + Leaves: employee: create/write/unlink not validated own [('employee_id.user_id','=',user.id), ('state', 'in', ['draft', 'confirm', 'cancel', 'refuse'])] - - Leaves Officer + + Leaves: officer: read all - ['|','|','|', ('employee_id.user_id', '=', user.id), ('department_id', '=', False), ('department_id.manager_id', '=', False), ('department_id.manager_id.user_id', '=', user.id)] - + [(1, '=', 1)] + + + + + + + + Leaves: officer: create/write/unlink own + managed employee + without department + same or managed department + + ['|', +'|', ('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)] + + - Manager Leaves + Leaves: manager: no limit [(1, '=', 1)] + + Leaves: multi company global rule + + + ['|', ('holiday_status_id.company_id', '=', False), ('holiday_status_id.company_id', 'in', [user.company_id.id])] + + - Employee Allocations + Allocations: employee: read own - [('employee_id.user_id','=',user.id)] + [('employee_id.user_id', '=', user.id)] - - Employee Allocations Create, Write, Unlink + + Allocations: employee: create/write/unlink not validated own [('employee_id.user_id','=',user.id), ('state', 'in', ['draft', 'confirm', 'cancel', 'refuse'])] - - Leaves Officer + + Allocations: officer: read all - ['|','|','|', ('employee_id.user_id', '=', user.id), ('department_id', '=', False), ('department_id.manager_id', '=', False), ('department_id.manager_id.user_id', '=', user.id)] + [(1, '=', 1)] + + + + + + + + Allocations: officer: create/write/unlink own + managed employee + without department + same or managed department + + ['|', +'|', ('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)] - Manager Allocations + Allocations: manager: no limit [(1, '=', 1)] diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py index 7bc7f93f42f..c8c2b4a438b 100644 --- a/addons/hr_holidays/tests/__init__.py +++ b/addons/hr_holidays/tests/__init__.py @@ -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 diff --git a/addons/hr_holidays/tests/common.py b/addons/hr_holidays/tests/common.py index 277d5c446c5..467eff2037c 100644 --- a/addons/hr_holidays/tests/common.py +++ b/addons/hr_holidays/tests/common.py @@ -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', diff --git a/addons/hr_holidays/tests/test_access_rights.py b/addons/hr_holidays/tests/test_access_rights.py new file mode 100644 index 00000000000..0631fa10006 --- /dev/null +++ b/addons/hr_holidays/tests/test_access_rights.py @@ -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') diff --git a/addons/hr_holidays/tests/test_holidays_flow.py b/addons/hr_holidays/tests/test_holidays_flow.py index 6bbb81d57c0..d48045d2613 100644 --- a/addons/hr_holidays/tests/test_holidays_flow.py +++ b/addons/hr_holidays/tests/test_holidays_flow.py @@ -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', diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml index 96e18b5436b..7b06b139ab5 100644 --- a/addons/hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_views.xml @@ -881,7 +881,7 @@ - + diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index ebdc5dd321a..b6952ec28d8 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -70,7 +70,7 @@ - + diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 6ba6c1ff62a..d5845003de2 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -58,7 +58,7 @@ index_template = """

%s

- The PosBox software installed on this posbox is version 17 BETA, + The PosBox software installed on this posbox is version 17, the posbox version number is independent from Odoo. You can upgrade the software on the upgrade page.

diff --git a/addons/l10n_th/data/l10n_th_chart_data.xml b/addons/l10n_th/data/l10n_th_chart_data.xml index b919aabc83d..b9bfefc6c13 100644 --- a/addons/l10n_th/data/l10n_th_chart_data.xml +++ b/addons/l10n_th/data/l10n_th_chart_data.xml @@ -352,7 +352,7 @@ Company Withholding Tax 1% (Transportation) percent - + purchase @@ -364,7 +364,7 @@ Company Withholding Tax 2% (Advertising) percent - + purchase @@ -376,7 +376,7 @@ Company Withholding Tax 3% (Service) percent - + purchase @@ -388,7 +388,7 @@ Company Withholding Tax 5% (Rental) percent - + purchase @@ -400,7 +400,7 @@ Personal Withholding Tax 1% (Transportation) percent - + purchase @@ -412,7 +412,7 @@ Personal Withholding Tax 2% (Advertising) percent - + purchase @@ -424,7 +424,7 @@ Personal Withholding Tax 3% (Service) percent - + purchase @@ -436,7 +436,7 @@ Personal Withholding Tax 5% (Rental) percent - + purchase @@ -448,7 +448,7 @@ Withholding Income Tax 1% (Transportation) percent - + sale @@ -459,7 +459,7 @@ Withholding Income Tax 2% (Advertising) percent - + sale @@ -470,7 +470,7 @@ Withholding Income Tax 3% (Service) percent - + sale @@ -481,7 +481,7 @@ Withholding Income Tax 5% (Rental) percent - + sale diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index b556a6ac66a..fe1dc27945a 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -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): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 6ab091e2f43..f7fd19d50a9 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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 diff --git a/addons/mass_mailing_event/models/event.py b/addons/mass_mailing_event/models/event.py index be37000e6f2..16b9c066862 100644 --- a/addons/mass_mailing_event/models/event.py +++ b/addons/mass_mailing_event/models/event.py @@ -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 \ No newline at end of file + return mass_mailing_action diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index 12c97db693a..5c6cf0e50a4 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -151,7 +151,7 @@ - + diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index f38b6535b9c..e4bd3382caa 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -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]] diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 852f5927c90..c791d112fa3 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -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): diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 91e234dafc5..df20d976efd 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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) { diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 95a7555e5d2..a50ccb14c0e 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -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() { diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 3382eb8bcb3..e70428b911b 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -375,7 +375,7 @@ -
+
@@ -384,7 +384,7 @@
Print receipts automatically once the payment is registered
-
+
Skip Preview Screen
diff --git a/addons/pos_sale/models/crm_team.py b/addons/pos_sale/models/crm_team.py index ffbfcd026c6..b8ed1c48710 100644 --- a/addons/pos_sale/models/crm_team.py +++ b/addons/pos_sale/models/crm_team.py @@ -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( diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index 06dd82e00ac..1c55345ceb4 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -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({ diff --git a/addons/project/models/project.py b/addons/project/models/project.py index d4eb770440a..cf2e2f62d55 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -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 diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index 02f4435e21f..70838455505 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -201,7 +201,7 @@
- +
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index a042ce1975b..e9d601ee9f7 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -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): diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 85e9b6238ca..3adfb19c426 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -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' diff --git a/addons/sale/i18n/sale.pot b/addons/sale/i18n/sale.pot index 3bec74b142b..eb01e619392 100644 --- a/addons/sale/i18n/sale.pot +++ b/addons/sale/i18n/sale.pot @@ -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 diff --git a/addons/sale/models/analytic.py b/addons/sale/models/analytic.py index f9c2d87afa9..c27337c6182 100644 --- a/addons/sale/models/analytic.py +++ b/addons/sale/models/analytic.py @@ -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) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index a818d997549..7ebf9f9c824 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -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: diff --git a/addons/sale_payment/controllers/payment.py b/addons/sale_payment/controllers/payment.py index 13d92577ead..09568f6527c 100644 --- a/addons/sale_payment/controllers/payment.py +++ b/addons/sale_payment/controllers/payment.py @@ -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 diff --git a/addons/sale_stock/report/stock_report_deliveryslip.xml b/addons/sale_stock/report/stock_report_deliveryslip.xml index ea8ca1c0612..893b898fa3c 100644 --- a/addons/sale_stock/report/stock_report_deliveryslip.xml +++ b/addons/sale_stock/report/stock_report_deliveryslip.xml @@ -2,10 +2,10 @@