diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index bbe63917..e511dedc 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -33,7 +33,7 @@ class DeliveryCarrier(models.Model): # Internals for shipping providers # # -------------------------------- # - name = fields.Char(required=True) + name = fields.Char(required=True, translate=True) active = fields.Boolean(default=True) sequence = fields.Integer(help="Determine the display order", default=10) # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex') diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index 8311da99..441ffe1f 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -91,7 +91,7 @@ class SaleOrder(models.Model): # Create the sales order line values = { 'order_id': self.id, - 'name': carrier.name, + 'name': carrier.with_context(lang=self.partner_id.lang).name, 'product_uom_qty': 1, 'product_uom': carrier.product_id.uom_id.id, 'product_id': carrier.product_id.id, @@ -117,3 +117,7 @@ class SaleOrderLine(models.Model): if not line.product_id or not line.product_uom or not line.product_uom_qty: return 0.0 line.product_qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) + + def _is_delivery(self): + self.ensure_one() + return self.is_delivery diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index 9fa0657c..6b36f334 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -161,8 +161,11 @@ class StockPicking(models.Model): def send_to_shipper(self): self.ensure_one() res = self.carrier_id.send_shipping(self)[0] + if self.carrier_id.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= self.carrier_id.amount: + res['exact_price'] = 0.0 self.carrier_price = res['exact_price'] - self.carrier_tracking_ref = res['tracking_number'] + if res['tracking_number']: + self.carrier_tracking_ref = res['tracking_number'] order_currency = self.sale_id.currency_id or self.company_id.currency_id msg = _("Shipment sent to carrier %s for shipping with tracking number %s
Cost: %.2f %s") % (self.carrier_id.name, self.carrier_tracking_ref, self.carrier_price, order_currency.name) self.message_post(body=msg) diff --git a/addons/delivery/tests/test_delivery_cost.py b/addons/delivery/tests/test_delivery_cost.py index f782376e..b359a25c 100644 --- a/addons/delivery/tests/test_delivery_cost.py +++ b/addons/delivery/tests/test_delivery_cost.py @@ -31,7 +31,10 @@ class TestDeliveryCost(common.TransactionCase): self.free_delivery = self.env.ref('delivery.free_delivery_carrier') # as the tests hereunder assume all the prices in USD, we must ensure # that the company actually uses USD - self.env.user.company_id.write({'currency_id': self.env.ref('base.USD').id}) + self.env.cr.execute( + "UPDATE res_company SET currency_id = %s WHERE id = %s", + [self.env.ref('base.USD').id, self.env.user.company_id.id]) + self.pricelist.currency_id = self.env.ref('base.USD').id def test_00_delivery_cost(self): # In order to test Carrier Cost diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 043fe888..a5b2f737 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -438,9 +438,14 @@ class EventRegistration(models.Model): @api.multi def message_get_suggested_recipients(self): recipients = super(EventRegistration, self).message_get_suggested_recipients() + public_users = self.env['res.users'].sudo() + public_groups = self.env.ref("base.group_public", raise_if_not_found=False) + if public_groups: + public_users = public_groups.sudo().with_context(active_test=False).mapped("users") try: for attendee in self: - if attendee.partner_id: + is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False + if attendee.partner_id and not is_public: attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer')) elif attendee.email: attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email')) diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index d6dc257c..8379a264 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -4,7 +4,12 @@ from datetime import datetime from dateutil.relativedelta import relativedelta from flectra import api, fields, models, tools +from flectra.tools import exception_to_unicode +from flectra.tools.translate import _ +import random +import logging +_logger = logging.getLogger(__name__) _INTERVALS = { 'hours': lambda interval: relativedelta(hours=interval), @@ -114,13 +119,52 @@ class EventMailScheduler(models.Model): self.write({'mail_sent': True}) return True + @api.model + def _warn_template_error(self, scheduler, exception): + # We warn ~ once by hour ~ instead of every 10 min if the interval unit is more than 'hours'. + if random.random() < 0.1666 or scheduler.interval_unit in ('now', 'hours'): + ex_s = exception_to_unicode(exception) + try: + event, template = scheduler.event_id, scheduler.template_id + emails = list(set([event.organizer_id.email, event.user_id.email, template.write_uid.email])) + subject = _("WARNING: Event Scheduler Error for event: %s" % event.name) + body = _("""Event Scheduler for: + - Event: %s (%s) + - Scheduled: %s + - Template: %s (%s) + + Failed with error: + - %s + + You receive this email because you are: + - the organizer of the event, + - or the responsible of the event, + - or the last writer of the template.""" + % (event.name, event.id, scheduler.scheduled_date, template.name, template.id, ex_s)) + email = self.env['ir.mail_server'].build_email( + email_from=self.env.user.email, + email_to=emails, + subject=subject, body=body, + ) + self.env['ir.mail_server'].send_email(email) + except Exception as e: + _logger.error("Exception while sending traceback by email: %s.\n Original Traceback:\n%s", e, exception) + pass + @api.model def run(self, autocommit=False): schedulers = self.search([('done', '=', False), ('scheduled_date', '<=', datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT))]) for scheduler in schedulers: - scheduler.execute() - if autocommit: - self.env.cr.commit() + try: + with self.env.cr.savepoint(): + scheduler.execute() + except Exception as e: + _logger.exception(e) + self.invalidate_cache() + self._warn_template_error(scheduler, e) + else: + if autocommit: + self.env.cr.commit() return True diff --git a/addons/event/models/res_partner.py b/addons/event/models/res_partner.py index ba52eefb..5d13e69c 100644 --- a/addons/event/models/res_partner.py +++ b/addons/event/models/res_partner.py @@ -9,6 +9,8 @@ class ResPartner(models.Model): event_count = fields.Integer("Events", compute='_compute_event_count', help="Number of events the partner has participated.") def _compute_event_count(self): + if not self.user_has_groups('event.group_event_user'): + return for partner in self: partner.event_count = self.env['event.event'].search_count([('registration_ids.partner_id', 'child_of', partner.ids)]) diff --git a/addons/event/report/event_event_templates.xml b/addons/event/report/event_event_templates.xml index f083b2b6..3094f9db 100644 --- a/addons/event/report/event_event_templates.xml +++ b/addons/event/report/event_event_templates.xml @@ -18,7 +18,7 @@
-
( to )
+
( to )
diff --git a/addons/event/security/event_security.xml b/addons/event/security/event_security.xml index 846e54d9..c88ac0f4 100644 --- a/addons/event/security/event_security.xml +++ b/addons/event/security/event_security.xml @@ -41,5 +41,12 @@ ] + + Event/Registration: Portal + + + ['|', ('email', '=', user.partner_id.email), ('partner_id', '=', user.partner_id.id)] + + diff --git a/addons/event/security/ir.model.access.csv b/addons/event/security/ir.model.access.csv index 49d859ad..8a49f01f 100644 --- a/addons/event/security/ir.model.access.csv +++ b/addons/event/security/ir.model.access.csv @@ -5,7 +5,8 @@ access_event_event_portal,event.event.portal,model_event_event,,1,0,0,0 access_event_event_user,event.event.user,model_event_event,event.group_event_user,1,0,0,0 access_event_event_manager,event.event.manager,model_event_event,event.group_event_manager,1,1,1,1 access_event_registration,event.registration,model_event_registration,event.group_event_user,1,1,1,1 -access_event_registration_portal,event.registration,model_event_registration,,1,0,0,0 +access_event_registration_employee,event.registration,model_event_registration,base.group_user,1,0,0,0 +access_event_registration_portal,event.registration,model_event_registration,base.group_portal,1,0,0,0 access_event_mail,event.mail,model_event_mail,event.group_event_user,1,0,0,0 access_event_mail_manager,event.mail manager,model_event_mail,event.group_event_manager,1,1,1,1 access_event_mail_registration,event.mail.registration,model_event_mail_registration,event.group_event_user,1,0,0,0 diff --git a/addons/event/views/res_partner_views.xml b/addons/event/views/res_partner_views.xml index 311e478c..56cf55a7 100644 --- a/addons/event/views/res_partner_views.xml +++ b/addons/event/views/res_partner_views.xml @@ -5,11 +5,13 @@ view.res.partner.form.event.inherited res.partner +
diff --git a/addons/event_sale/__manifest__.py b/addons/event_sale/__manifest__.py index 45918419..4567054f 100644 --- a/addons/event_sale/__manifest__.py +++ b/addons/event_sale/__manifest__.py @@ -27,6 +27,7 @@ this event. 'data/event_sale_data.xml', 'report/event_event_templates.xml', 'security/ir.model.access.csv', + 'security/event_security.xml', 'wizard/event_edit_registration.xml', ], 'demo': ['data/event_demo.xml'], diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 3053bcd5..a925883a 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -41,7 +41,7 @@ class SaleOrderLine(models.Model): order line has a product_uom_qty attribute that will be the number of registrations linked to this line. This method update existing registrations and create new one for missing one. """ - Registration = self.env['event.registration'] + Registration = self.env['event.registration'].sudo() registrations = Registration.search([('sale_order_line_id', 'in', self.ids), ('state', '!=', 'cancel')]) for so_line in self.filtered('event_id'): existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id) diff --git a/addons/event_sale/security/event_security.xml b/addons/event_sale/security/event_security.xml new file mode 100644 index 00000000..34ae8f23 --- /dev/null +++ b/addons/event_sale/security/event_security.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index 7c09f595..e40f5a3b 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -19,9 +19,10 @@ class FleetVehicle(models.Model): name = fields.Char(compute="_compute_vehicle_name", store=True) active = fields.Boolean('Active', default=True, track_visibility="onchange") company_id = fields.Many2one('res.company', 'Company') - license_plate = fields.Char(required=True, help='License plate number of the vehicle (i = plate number for a car)') + license_plate = fields.Char(required=True, track_visibility="onchange", + help='License plate number of the vehicle (i = plate number for a car)') vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False) - driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle', copy=False) + driver_id = fields.Many2one('res.partner', 'Driver', track_visibility="onchange", help='Driver of the vehicle', copy=False) model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle') log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs') log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs') @@ -74,10 +75,10 @@ class FleetVehicle(models.Model): ('driver_id_unique', 'UNIQUE(driver_id)', 'Only one car can be assigned to the same employee!') ] - @api.depends('model_id', 'license_plate') + @api.depends('model_id.brand_id.name', 'model_id.name', 'license_plate') def _compute_vehicle_name(self): for record in self: - record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate + record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + (record.license_plate or _('No Plate')) def _get_odometer(self): FleetVehicalOdometer = self.env['fleet.vehicle.odometer'] diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 60988193..27b724b8 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -259,7 +259,7 @@ class Goal(models.Model): If the end date is passed (at least +1 day, time not considered) without the target value being reached, the goal is set as failed.""" goals_by_definition = {} - for goal in self: + for goal in self.with_context(prefetch_fields=False): goals_by_definition.setdefault(goal.definition_id, []).append(goal) for definition, goals in goals_by_definition.items(): diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index e5a12b9c..c3d125a1 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -677,7 +677,7 @@ class GoogleCalendar(models.AbstractModel): try: all_event_from_google = self.get_event_synchro_dict(lastSync=lastSync) except requests.HTTPError as e: - if e.response.code == 410: # GONE, Google is lost. + if e.response.status_code == 410: # GONE, Google is lost. # we need to force the rollback from this cursor, because it locks my res_users but I need to write in this tuple before to raise. self.env.cr.rollback() self.env.user.write({'google_calendar_last_sync_date': False}) @@ -842,7 +842,7 @@ class GoogleCalendar(models.AbstractModel): try: # if already deleted from gmail or never created recs.delete_an_event(current_event[0]) - except Exception as e: + except requests.exceptions.HTTPError as e: if e.response.status_code in (401, 410,): pass else: diff --git a/addons/google_calendar/views/res_config_settings_views.xml b/addons/google_calendar/views/res_config_settings_views.xml index fff9afd4..fff91b89 100644 --- a/addons/google_calendar/views/res_config_settings_views.xml +++ b/addons/google_calendar/views/res_config_settings_views.xml @@ -24,6 +24,7 @@ ir.actions.act_window res.config.settings form + {'module' : 'general_settings'} inline diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index c2c1fdfd..228593c9 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -2,10 +2,12 @@ # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. from collections import OrderedDict +from operator import itemgetter from flectra import http, _ from flectra.http import request from flectra.addons.portal.controllers.portal import get_records_pager, CustomerPortal, pager as portal_pager +from flectra.tools import groupby as groupbyelem from flectra.osv.expression import OR @@ -78,6 +80,7 @@ class CustomerPortal(CustomerPortal): @http.route(['/my/tasks', '/my/tasks/page/'], type='http', auth="user", website=True) def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', **kw): + groupby = kw.get('groupby', 'project') #TODO master fix this values = self._prepare_portal_layout_values() searchbar_sortings = { 'date': {'label': _('Newest'), 'order': 'create_date desc'}, @@ -95,6 +98,10 @@ class CustomerPortal(CustomerPortal): 'stage': {'input': 'stage', 'label': _('Search in Stages')}, 'all': {'input': 'all', 'label': _('Search in All')}, } + searchbar_groupby = { + 'none': {'input': 'none', 'label': _('None')}, + 'project': {'input': 'project', 'label': _('Project')}, + } # 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')]) @@ -136,28 +143,37 @@ class CustomerPortal(CustomerPortal): # pager pager = portal_pager( url="/my/tasks", - url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby}, + url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby, 'search_in': search_in, 'search': search}, total=task_count, page=page, step=self._items_per_page ) # content according to pager and archive selected + if groupby == 'project': + order = "project_id, %s" % order # force sort on project first to group by project in view tasks = request.env['project.task'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset']) request.session['my_tasks_history'] = tasks.ids[:100] + if groupby == 'project': + grouped_tasks = [request.env['project.task'].concat(*g) for k, g in groupbyelem(tasks, itemgetter('project_id'))] + else: + grouped_tasks = [tasks] values.update({ 'date': date_begin, 'date_end': date_end, 'projects': projects, 'tasks': tasks, + 'grouped_tasks': grouped_tasks, 'page_name': 'task', 'archive_groups': archive_groups, 'default_url': '/my/tasks', 'pager': pager, 'searchbar_sortings': searchbar_sortings, + 'searchbar_groupby': searchbar_groupby, 'searchbar_inputs': searchbar_inputs, 'search_in': search_in, 'sortby': sortby, + 'groupby': groupby, 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, }) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 61c3906f..e6e1d444 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -4,7 +4,7 @@ from lxml import etree from flectra import api, fields, models, tools, SUPERUSER_ID, _ -from flectra.exceptions import UserError, AccessError +from flectra.exceptions import UserError, AccessError, ValidationError from flectra.tools.safe_eval import safe_eval from datetime import timedelta, date @@ -469,7 +469,7 @@ class Task(models.Model): legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False) legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False) parent_id = fields.Many2one('project.task', string='Parent Task', index=True) - child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks") + child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False}) subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True) subtask_count = fields.Integer(compute='_compute_subtask_count', type='integer', string="Sub-task count") email_from = fields.Char(string='Email', help="These people will receive email.", index=True) @@ -598,6 +598,12 @@ class Task(models.Model): for task in self: task.subtask_count = self.search_count([('id', 'child_of', task.id), ('id', '!=', task.id)]) + @api.constrains('parent_id') + def _check_parent_id(self): + for task in self: + if not task._check_recursion(): + raise ValidationError(_('Error! You cannot create recursive hierarchy of task(s).')) + @api.constrains('parent_id') def _check_subtask_project(self): for task in self: @@ -796,7 +802,7 @@ class Task(models.Model): groups = [new_group] + groups for group_name, group_method, group_data in groups: - if group_name in ['customer', 'portal']: + if group_name == 'customer': continue group_data['has_button_access'] = True @@ -874,12 +880,12 @@ class Task(models.Model): @api.multi def message_get_suggested_recipients(self): recipients = super(Task, self).message_get_suggested_recipients() - for task in self.filtered('partner_id'): - reason = _('Customer Email') if task.partner_id.email else _('Customer') + for task in self: if task.partner_id: + reason = _('Customer Email') if task.partner_id.email else _('Customer') task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason) elif task.email_from: - task._message_add_suggested_recipient(recipients, partner=task.email_from, reason=reason) + task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email')) return recipients @api.multi @@ -926,6 +932,20 @@ class Task(models.Model): 'type': 'ir.actions.act_window' } + def action_subtask(self): + action = self.env.ref('project.project_task_action_sub_task').read()[0] + ctx = self.env.context.copy() + ctx.update({ + 'default_parent_id' : self.id, + 'default_project_id' : self.env.context.get('project_id', self.subtask_project_id.id), + 'default_name' : self.env.context.get('name', self.name) + ':', + 'default_partner_id' : self.env.context.get('partner_id', self.partner_id.id), + 'search_default_project_id': self.env.context.get('project_id', self.subtask_project_id.id), + }) + action['context'] = ctx + action['domain'] = [('id', 'child_of', self.id), ('id', '!=', self.id)] + return action + class AccountAnalyticAccount(models.Model): _inherit = 'account.analytic.account' diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index 414bb61b..b802e6ef 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -130,36 +130,41 @@

Tasks

- + -
-
- - - - - - - - - - - - - - - - -
TaskStageRef
- - - - - -
+
+
+ + + + + + + + + + + + + + + + + + + +
Name for project: StageRef
+ + + + + +
+
+
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 3aac08d4..7622d58b 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -413,7 +413,7 @@