diff --git a/addons/sale_timesheet/data/sale_service_demo.xml b/addons/sale_timesheet/data/sale_service_demo.xml index 92d989fb2eb..bde8f354b42 100644 --- a/addons/sale_timesheet/data/sale_service_demo.xml +++ b/addons/sale_timesheet/data/sale_service_demo.xml @@ -1,5 +1,5 @@ - + timesheet @@ -32,4 +32,4 @@ - + diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 0250d4252b4..c4e4573d7cb 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -1,12 +1,48 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import models, api +from odoo import api, models class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' + @api.model + def create(self, values): + if values.get('task_id'): + task = self.env['project.task'].browse(values['task_id']) + values['so_line'] = task.sale_line_id.id or values.get('so_line', False) + values.update(self._get_timesheet_cost(values)) + return super(AccountAnalyticLine, self).create(values) + + @api.multi + def write(self, values): + if values.get('task_id'): + task = self.env['project.task'].browse(values['task_id']) + values['so_line'] = task.sale_line_id.id or values.get('so_line', False) + for line in self: + values.update(line._get_timesheet_cost(values)) + super(AccountAnalyticLine, line).write(values) + return True + + def _get_timesheet_cost(self, values): + values = values if values is not None else {} + if values.get('project_id') or self.project_id: + if values.get('amount'): + return {} + unit_amount = values.get('unit_amount', 0.0) or self.unit_amount + user_id = values.get('user_id') or self.user_id.id or self._default_user() + user = self.env['res.users'].browse([user_id]) + emp = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1) + cost = emp and emp.timesheet_cost or 0.0 + uom = (emp or user).company_id.project_time_mode_id + # Nominal employee cost = 1 * company project UoM (project_time_mode_id) + return { + 'amount': -unit_amount * cost, + 'product_uom_id': uom.id + } + return {} + def _get_sale_order_line(self, vals=None): result = dict(vals or {}) if self.project_id: @@ -26,45 +62,6 @@ class AccountAnalyticLine(models.Model): 'so_line': sol.id, 'product_id': sol.product_id.id, }) - result = self._get_timesheet_cost(result) + result.update(self._get_timesheet_cost(result)) - result = super(AccountAnalyticLine, self)._get_sale_order_line(vals=result) - return result - - def _get_timesheet_cost(self, vals=None): - result = dict(vals or {}) - if result.get('project_id') or self.project_id: - if result.get('amount'): - return result - unit_amount = result.get('unit_amount', 0.0) or self.unit_amount - user_id = result.get('user_id') or self.user_id.id or self._default_user() - user = self.env['res.users'].browse([user_id]) - emp = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1) - cost = emp and emp.timesheet_cost or 0.0 - uom = (emp or user).company_id.project_time_mode_id - # Nominal employee cost = 1 * company project UoM (project_time_mode_id) - result.update( - amount=(-unit_amount * cost), - product_uom_id=uom.id - ) - return result - - @api.model - def _update_values(self, values): - if values.get('task_id', False): - task = self.env['project.task'].browse(values['task_id']) - values['so_line'] = task.sale_line_id and task.sale_line_id.id or values.get('so_line', False) - - @api.multi - def write(self, values): - self._update_values(values) - for line in self: - values = line._get_timesheet_cost(vals=values) - super(AccountAnalyticLine, line).write(values) - return True - - @api.model - def create(self, values): - self._update_values(values) - values = self._get_timesheet_cost(vals=values) - return super(AccountAnalyticLine, self).create(values) + return super(AccountAnalyticLine, self)._get_sale_order_line(vals=result) diff --git a/addons/sale_timesheet/models/hr.py b/addons/sale_timesheet/models/hr.py index 5ee69e82d16..546b3d21534 100644 --- a/addons/sale_timesheet/models/hr.py +++ b/addons/sale_timesheet/models/hr.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import models, fields +from odoo import fields, models class HrEmployee(models.Model): _inherit = 'hr.employee' + # FIXME: this field should be in module hr_timesheet, not sale_timesheet - timesheet_cost = fields.Float(string='Timesheet Cost', default=0.0) + timesheet_cost = fields.Float('Timesheet Cost', default=0.0) diff --git a/addons/sale_timesheet/models/procurement.py b/addons/sale_timesheet/models/procurement.py index ba07285cb34..b8d34d70fd1 100644 --- a/addons/sale_timesheet/models/procurement.py +++ b/addons/sale_timesheet/models/procurement.py @@ -1,85 +1,76 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools.translate import _ +from odoo import api, fields, models, _ -class procurement_order(osv.osv): - _name = "procurement.order" - _inherit = "procurement.order" - _columns = { - 'task_id': fields.many2one('project.task', 'Task', copy=False), - } +class ProcurementOrder(models.Model): + _inherit = 'procurement.order' - def _is_procurement_task(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - return procurement.product_id.type == 'service' and procurement.product_id.track_service=='task' or False + task_id = fields.Many2one('project.task', 'Task', copy=False) - def _assign(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - res = super(procurement_order, self)._assign(cr, uid, ids, context=context) + def _is_procurement_task(self): + return self.product_id.type == 'service' and self.product_id.track_service == 'task' + + @api.multi + def _assign(self): + self.ensure_one() + res = super(ProcurementOrder, self)._assign() if not res: - #if there isn't any specific procurement.rule defined for the product, we may want to create a task - return procurement._is_procurement_task() + # if there isn't any specific procurement.rule defined for the product, we may want to create a task + return self._is_procurement_task() return res - def _run(self, cr, uid, ids, context=None): - procurement = self.browse(cr, uid, ids[0], context=context) - if procurement._is_procurement_task() and not procurement.task_id: - #create a task for the procurement - return self._create_service_task(cr, uid, procurement, context=context) - return super(procurement_order, self)._run(cr, uid, ids, context=context) + @api.multi + def _run(self): + self.ensure_one() + if self._is_procurement_task() and not self.task_id: + # create a task for the procurement + return self._create_service_task() + return super(ProcurementOrder, self)._run() - def _convert_qty_company_hours(self, cr, uid, procurement, context=None): - product_uom = self.pool.get('product.uom') - company_time_uom_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.project_time_mode_id - if procurement.product_uom.id != company_time_uom_id.id and procurement.product_uom.category_id.id == company_time_uom_id.category_id.id: - planned_hours = product_uom._compute_quantity(cr, uid, [procurement.product_uom.id], procurement.product_qty, company_time_uom_id) + def _convert_qty_company_hours(self): + company_time_uom_id = self.env.user.company_id.project_time_mode_id + if self.product_uom.id != company_time_uom_id.id and self.product_uom.category_id.id == company_time_uom_id.category_id.id: + planned_hours = self.product_uom._compute_quantity(self.product_qty, company_time_uom_id) else: - planned_hours = procurement.product_qty + planned_hours = self.product_qty return planned_hours - def _get_project(self, cr, uid, procurement, context=None): - project_project = self.pool.get('project.project') - project = procurement.product_id.project_id - if not project and procurement.sale_line_id: + def _get_project(self): + Project = self.env['project.project'] + project = self.product_id.project_id + if not project and self.sale_line_id: # find the project corresponding to the analytic account of the sales order - account = procurement.sale_line_id.order_id.project_id + account = self.sale_line_id.order_id.project_id if not account: - procurement.sale_line_id.order_id._create_analytic_account() - account = procurement.sale_line_id.order_id.project_id - project_ids = project_project.search(cr, uid, [('analytic_account_id', '=', account.id)]) - projects = project_project.browse(cr, uid, project_ids, context=context) - project = projects and projects[0] + self.sale_line_id.order_id._create_analytic_account() + account = self.sale_line_id.order_id.project_id + project = Project.search([('analytic_account_id', '=', account.id)], limit=1) if not project: project_id = account.project_create({'name': account.name, 'use_tasks': True}) - project = project_project.browse(cr, uid, project_id, context=context) + project = Project.browse(project_id) return project - def _create_service_task(self, cr, uid, procurement, context=None): - project_task = self.pool.get('project.task') - project = self._get_project(cr, uid, procurement, context=context) - planned_hours = self._convert_qty_company_hours(cr, uid, procurement, context=context) - task_id = project_task.create(cr, uid, { - 'name': '%s:%s' % (procurement.origin or '', procurement.product_id.name), - 'date_deadline': procurement.date_planned, + def _create_service_task(self): + project = self._get_project() + planned_hours = self._convert_qty_company_hours() + task = self.env['project.task'].create({ + 'name': '%s:%s' % (self.origin or '', self.product_id.name), + 'date_deadline': self.date_planned, 'planned_hours': planned_hours, 'remaining_hours': planned_hours, - 'partner_id': procurement.sale_line_id and procurement.sale_line_id.order_id.partner_id.id or procurement.partner_dest_id.id, - 'user_id': procurement.product_id.product_manager.id, - 'procurement_id': procurement.id, - 'description': procurement.name + '\n', - 'project_id': project and project.id or False, - 'company_id': procurement.company_id.id, - },context=context) - self.write(cr, uid, [procurement.id], {'task_id': task_id}, context=context) - self.project_task_create_note(cr, uid, [procurement.id], context=context) - return task_id + 'partner_id': self.sale_line_id.order_id.partner_id.id or self.partner_dest_id.id, + 'user_id': self.product_id.product_manager.id, + 'procurement_id': self.id, + 'description': self.name + '\n', + 'project_id': project.id, + 'company_id': self.company_id.id, + }) + self.write({'task_id': task.id}) - def project_task_create_note(self, cr, uid, ids, context=None): - for procurement in self.browse(cr, uid, ids, context=context): - body = _("Task created") - self.message_post(cr, uid, [procurement.id], body=body, context=context) - if procurement.sale_line_id and procurement.sale_line_id.order_id: - procurement.sale_line_id.order_id.message_post(body=body) + self.message_post(body=_("Task created")) + if self.sale_line_id.order_id: + self.sale_line_id.order_id.message_post(body=_("Task created")) + + return task diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py index bc67ec9b68c..e001e0ad47a 100644 --- a/addons/sale_timesheet/models/product.py +++ b/addons/sale_timesheet/models/product.py @@ -1,23 +1,25 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import api, fields, models +from odoo import api, fields, models class ProductTemplate(models.Model): _inherit = 'product.template' - track_service = fields.Selection(selection_add=[('timesheet', 'Timesheets on project'), ('task', 'Create a task and track hours')]) - project_id = fields.Many2one('project.project', string='Project', - help='Create a task under this project on sale order validation. This setting must be set for each company.', - company_dependent=True) - @api.onchange('type', 'invoice_policy') - def onchange_type_timesheet(self): + track_service = fields.Selection(selection_add=[ + ('timesheet', 'Timesheets on project'), + ('task', 'Create a task and track hours')]) + project_id = fields.Many2one( + 'project.project', 'Project', company_dependent=True, + help='Create a task under this project on sale order validation. This setting must be set for each company.') + + @api.onchange('type') + def _onchange_type(self): if self.type == 'service': self.track_service = 'timesheet' else: self.track_service = 'manual' - return {} class ProductProduct(models.Model): diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 8b6637ece2c..c52d5c228b7 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -1,45 +1,34 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.exceptions import UserError -from openerp.tools.translate import _ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError -class project_task(osv.osv): - _name = "project.task" +class ProjectTask(models.Model): _inherit = "project.task" - _columns = { - 'procurement_id': fields.many2one('procurement.order', 'Procurement', ondelete='set null'), - 'sale_line_id': fields.related('procurement_id', 'sale_line_id', type='many2one', relation='sale.order.line', store=True, string='Sales Order Line'), - } - def unlink(self, cr, uid, ids, context=None): - if context is None: - context = {} - for task in self.browse(cr, uid, ids, context=context): - if task.sale_line_id: - raise UserError(_('You cannot delete a task related to a Sale Order. You can only archive this task.')) - res = super(project_task, self).unlink(cr, uid, ids, context) - return res + procurement_id = fields.Many2one('procurement.order', 'Procurement', ondelete='set null') + sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Line', related='procurement_id.sale_line_id', store=True) - def action_view_so(self, cr, uid, ids, context=None): - task = self.browse(cr, uid, ids, context=context)[0] + @api.multi + def unlink(self): + if any(task.sale_line_id for task in self): + raise ValidationError(_('You cannot delete a task related to a Sale Order. You can only archive this task.')) + return super(ProjectTask, self).unlink() + + @api.multi + def action_view_so(self): + self.ensure_one() return { "type": "ir.actions.act_window", "res_model": "sale.order", "views": [[False, "form"]], - "res_id": task.sale_line_id.order_id.id, + "res_id": self.sale_line_id.order_id.id, "context": {"create": False, "show_sale": True}, } - def onchange_parent_id(self, cr, uid, ids, parent_id, context=None): - if not parent_id: - return {'value' : {'procurement_id': False, 'sale_line_id': False }} - parent_task = self.browse(cr, uid, parent_id, context=context) - return { - 'value' : { - 'procurement_id' : parent_task.procurement_id.id, - 'sale_line_id' : parent_task.sale_line_id.id, - } - } + @api.onchange('parent_id') + def onchange_parent_id(self): + self.procurement_id = self.parent_id.procurement_id.id + self.sale_line_id = self.parent_id.sale_line_id.id diff --git a/addons/sale_timesheet/models/res_company.py b/addons/sale_timesheet/models/res_company.py index f8b279a8225..19d5ce45ea0 100644 --- a/addons/sale_timesheet/models/res_company.py +++ b/addons/sale_timesheet/models/res_company.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import models, api, fields +from odoo import models, api, fields class ResCompany(models.Model): _inherit = 'res.company' @api.model - def _get_uom_hours(self): - try: - return self.env.ref("product.product_uom_hour") - except ValueError: - return False - project_time_mode_id = fields.Many2one('product.uom', string='Timesheet UoM', default=_get_uom_hours) + def _get_default_uom_id(self): + return self.env.ref("product.product_uom_hour", raise_if_not_found=False) + + project_time_mode_id = fields.Many2one('product.uom', 'Timesheet UoM', default=_get_default_uom_id) diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 51f288ca399..1ea09798345 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import models, api, fields -from openerp.tools.translate import _ +from odoo import api, fields, models, _ -from openerp.exceptions import ValidationError +from odoo.exceptions import ValidationError class SaleOrder(models.Model): @@ -31,6 +30,19 @@ class SaleOrder(models.Model): order.timesheet_ids = [] order.timesheet_count = len(order.timesheet_ids) + @api.multi + @api.depends('order_line.product_id.project_id') + def _compute_tasks_ids(self): + for order in self: + order.tasks_ids = self.env['project.task'].search([('sale_line_id', 'in', order.order_line.ids)]) + order.tasks_count = len(order.tasks_ids) + + @api.multi + @api.depends('project_id.project_ids') + def _compute_project_project_id(self): + for order in self: + order.project_project_id = self.env['project.project'].search([('analytic_account_id', '=', order.project_id.id)]) + @api.multi @api.constrains('order_line') def _check_multi_timesheet(self): @@ -44,19 +56,24 @@ class SaleOrder(models.Model): return {} @api.multi - @api.depends('order_line.product_id.project_id') - def _compute_tasks_ids(self): + def action_confirm(self): + result = super(SaleOrder, self).action_confirm() for order in self: - order.tasks_ids = self.env['project.task'].search([('sale_line_id', 'in', order.order_line.ids)]) - order.tasks_count = len(order.tasks_ids) + if not order.project_project_id: + for line in order.order_line: + if line.product_id.track_service == 'timesheet': + if not order.project_id: + order._create_analytic_account(prefix=line.product_id.default_code or None) + order.project_id.project_create({'name': order.project_id.name, 'use_tasks': True}) + break + return result @api.multi def action_view_task(self): self.ensure_one() - imd = self.env['ir.model.data'] - action = imd.xmlid_to_object('project.action_view_task') - list_view_id = imd.xmlid_to_res_id('project.view_task_tree2') - form_view_id = imd.xmlid_to_res_id('project.view_task_form2') + action = self.env.ref('project.action_view_task') + list_view_id = self.env.ref('project.view_task_tree2').id + form_view_id = self.env.ref('project.view_task_form2').id result = { 'name': action.name, @@ -76,18 +93,11 @@ class SaleOrder(models.Model): result = {'type': 'ir.actions.act_window_close'} return result - @api.multi - @api.depends('project_id.project_ids') - def _compute_project_project_id(self): - for order in self: - order.project_project_id = self.env['project.project'].search([('analytic_account_id', '=', order.project_id.id)]) - @api.multi def action_view_project_project(self): self.ensure_one() - imd = self.env['ir.model.data'] - action = imd.xmlid_to_object('project.open_view_project_all') - form_view_id = imd.xmlid_to_res_id('project.edit_project') + action = self.env.ref('project.open_view_project_all') + form_view_id = self.env.ref('project.edit_project').id result = { 'name': action.name, @@ -101,26 +111,12 @@ class SaleOrder(models.Model): } return result - @api.multi - def action_confirm(self): - result = super(SaleOrder, self).action_confirm() - for order in self: - if not order.project_project_id: - for line in order.order_line: - if line.product_id.track_service == 'timesheet': - if not order.project_id: - order._create_analytic_account(prefix=line.product_id.default_code or None) - order.project_id.project_create({'name': order.project_id.name, 'use_tasks': True}) - break - return result - @api.multi def action_view_timesheet(self): self.ensure_one() - imd = self.env['ir.model.data'] - action = imd.xmlid_to_object('hr_timesheet.act_hr_timesheet_line') - list_view_id = imd.xmlid_to_res_id('hr_timesheet.hr_timesheet_line_tree') - form_view_id = imd.xmlid_to_res_id('hr_timesheet.hr_timesheet_line_form') + action = self.env.ref('hr_timesheet.act_hr_timesheet_line') + list_view_id = self.env.ref('hr_timesheet.hr_timesheet_line_tree').id + form_view_id = self.env.ref('hr_timesheet.hr_timesheet_line_form').id result = { 'name': action.name, @@ -141,16 +137,16 @@ class SaleOrder(models.Model): class SaleOrderLine(models.Model): _inherit = "sale.order.line" - @api.multi - def _compute_analytic(self, domain=None): - if not domain: - # To filter on analyic lines linked to an expense - domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('project_id', '!=', False)] - return super(SaleOrderLine, self)._compute_analytic(domain=domain) - @api.model def create(self, values): line = super(SaleOrderLine, self).create(values) if line.state == 'sale' and not line.order_id.project_id and line.product_id.track_service in ['timesheet', 'task']: line.order_id._create_analytic_account() return line + + @api.multi + def _compute_analytic(self, domain=None): + if not domain: + # To filter on analyic lines linked to an expense + domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('project_id', '!=', False)] + return super(SaleOrderLine, self)._compute_analytic(domain=domain) diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index ee9e23feea1..218eaafb30a 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.addons.sale.tests.test_sale_common import TestSale +from odoo.addons.sale.tests.test_sale_common import TestSale class TestSaleService(TestSale): diff --git a/addons/sale_timesheet/tests/test_sale_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py index 04b6a10afa9..309650aee2f 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.tools import float_is_zero +from odoo.tools import float_is_zero -from openerp.addons.sale.tests.test_sale_common import TestSale +from odoo.addons.sale.tests.test_sale_common import TestSale class TestSaleTimesheet(TestSale): diff --git a/addons/sale_timesheet/views/hr_views.xml b/addons/sale_timesheet/views/hr_views.xml index bbba04e7282..01578bb5d07 100644 --- a/addons/sale_timesheet/views/hr_views.xml +++ b/addons/sale_timesheet/views/hr_views.xml @@ -1,17 +1,15 @@ - - - - hr.timesheet.employee.extd_form - hr.employee - - - - - - - - - - - + + + hr.timesheet.employee.extd_form + hr.employee + + + + + + + + + + diff --git a/addons/sale_timesheet/views/procurement_views.xml b/addons/sale_timesheet/views/procurement_views.xml index 63843516c0b..393ef3c8fba 100644 --- a/addons/sale_timesheet/views/procurement_views.xml +++ b/addons/sale_timesheet/views/procurement_views.xml @@ -1,15 +1,13 @@ - - - - procurement.procurement.form.view.inherit - procurement.order - - - - - + + + procurement.procurement.form.view.inherit + procurement.order + + + + - - - + + + \ No newline at end of file diff --git a/addons/sale_timesheet/views/product_views.xml b/addons/sale_timesheet/views/product_views.xml index 00d08ada78f..5525ae66d73 100644 --- a/addons/sale_timesheet/views/product_views.xml +++ b/addons/sale_timesheet/views/product_views.xml @@ -1,19 +1,17 @@ - - - - product.template.timesheet.form - product.template - - - - False - {'invisible': [('type','!=','service')]} - - - - + + + product.template.timesheet.form + product.template + + + + False + {'invisible': [('type','!=','service')]} + + + - - - + + + \ No newline at end of file diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 9b954edcf4a..6b4f7dc435d 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -1,6 +1,5 @@ - - + sale.service.form.view.inherit project.task @@ -17,10 +16,6 @@ - - onchange_parent_id(parent_id) - - - + diff --git a/addons/sale_timesheet/views/sale_order_views.xml b/addons/sale_timesheet/views/sale_order_views.xml index 220ae93752b..4a782672e23 100644 --- a/addons/sale_timesheet/views/sale_order_views.xml +++ b/addons/sale_timesheet/views/sale_order_views.xml @@ -1,6 +1,5 @@ - - + sale.order.form.sale.timesheet sale.order @@ -39,5 +38,4 @@ - - +