diff --git a/addons/account/models/account_analytic_line.py b/addons/account/models/account_analytic_line.py index 92f19595a42..58395a38b38 100644 --- a/addons/account/models/account_analytic_line.py +++ b/addons/account/models/account_analytic_line.py @@ -38,7 +38,7 @@ class AccountAnalyticAccount(models.Model): ('analytic_account_id', 'in', self.ids) ] groups = self.env['account.move.line'].read_group(domain, ['move_id:count_distinct'], ['analytic_account_id']) - moves_count_mapping = dict((g['analytic_account_id'][0], g['analytic_account_id_count']) for g in groups) + moves_count_mapping = dict((g['analytic_account_id'][0], g['move_id']) for g in groups) for account in self: account.invoice_count = moves_count_mapping.get(account.id, 0) @@ -51,7 +51,7 @@ class AccountAnalyticAccount(models.Model): ('analytic_account_id', 'in', self.ids) ] groups = self.env['account.move.line'].read_group(domain, ['move_id:count_distinct'], ['analytic_account_id']) - moves_count_mapping = dict((g['analytic_account_id'][0], g['analytic_account_id_count']) for g in groups) + moves_count_mapping = dict((g['analytic_account_id'][0], g['move_id']) for g in groups) for account in self: account.vendor_bill_count = moves_count_mapping.get(account.id, 0) diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index b434f4d20b2..e138e442bf4 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -71,8 +71,6 @@ class ResConfigSettings(models.TransientModel): domain=lambda self: [('reconcile', '=', True), ('user_type_id.id', '=', self.env.ref('account.data_account_type_current_assets').id)], help="Intermediary account used when moving from a liquidity account to another.") module_account_accountant = fields.Boolean(string='Accounting') - group_analytic_accounting = fields.Boolean(string='Analytic Accounting', - implied_group='analytic.group_analytic_accounting') group_analytic_tags = fields.Boolean(string='Analytic Tags', implied_group='analytic.group_analytic_tags') group_warning_account = fields.Boolean(string="Warnings in Invoices", implied_group='account.group_warning_account') group_cash_rounding = fields.Boolean(string="Cash Rounding", implied_group='account.group_cash_rounding') diff --git a/addons/sale_timesheet_purchase/__init__.py b/addons/account_sale_timesheet/__init__.py similarity index 100% rename from addons/sale_timesheet_purchase/__init__.py rename to addons/account_sale_timesheet/__init__.py diff --git a/addons/account_sale_timesheet/__manifest__.py b/addons/account_sale_timesheet/__manifest__.py new file mode 100644 index 00000000000..792e8e8fb86 --- /dev/null +++ b/addons/account_sale_timesheet/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Account Sale Timesheet', + 'version': '1.0', + 'category': 'Account/sale/timesheet', + 'summary': 'Account sale timesheet', + 'description': 'Bridge created to add the number of invoices linked to an AA to a project form', + 'depends': ['account', 'sale_timesheet'], + 'data': [ + 'views/project_project_views.xml', + ], + 'demo': [], + 'installable': True, + 'auto_install': True, + 'license': 'OEEL-1', +} diff --git a/addons/sale_timesheet_purchase/models/__init__.py b/addons/account_sale_timesheet/models/__init__.py similarity index 76% rename from addons/sale_timesheet_purchase/models/__init__.py rename to addons/account_sale_timesheet/models/__init__.py index 2e9481d9165..527e2954c06 100644 --- a/addons/sale_timesheet_purchase/models/__init__.py +++ b/addons/account_sale_timesheet/models/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import project_overview +from . import project diff --git a/addons/account_sale_timesheet/models/project.py b/addons/account_sale_timesheet/models/project.py new file mode 100644 index 00000000000..d8086b57a4c --- /dev/null +++ b/addons/account_sale_timesheet/models/project.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, _ + +class Project(models.Model): + _inherit = 'project.project' + + invoice_count = fields.Integer(related='analytic_account_id.invoice_count') + + # ---------------------------- + # Project Updates + # ---------------------------- + + def _get_stat_buttons(self): + buttons = super(Project, self)._get_stat_buttons() + buttons.append({ + 'icon': 'pencil-square-o', + 'text': _('Invoices'), + 'number': self.invoice_count, + 'action_type': 'object', + 'action': 'action_open_project_invoices', + 'show': self.user_has_groups('account.group_account_readonly') and bool(self.analytic_account_id) and self.invoice_count > 0, + 'sequence': 11, + }) + return buttons diff --git a/addons/account_sale_timesheet/views/project_project_views.xml b/addons/account_sale_timesheet/views/project_project_views.xml new file mode 100644 index 00000000000..77a119d9b6a --- /dev/null +++ b/addons/account_sale_timesheet/views/project_project_views.xml @@ -0,0 +1,26 @@ + + + + + project.project.form.inherit + project.project + + + + + + + + + diff --git a/addons/analytic/models/__init__.py b/addons/analytic/models/__init__.py index d1fd64b96f8..c85a3b29ee3 100644 --- a/addons/analytic/models/__init__.py +++ b/addons/analytic/models/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import analytic_account +from . import res_config_settings diff --git a/addons/analytic/models/res_config_settings.py b/addons/analytic/models/res_config_settings.py new file mode 100644 index 00000000000..abe5e7e96a0 --- /dev/null +++ b/addons/analytic/models/res_config_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + group_analytic_accounting = fields.Boolean(string='Analytic Accounting', implied_group='analytic.group_analytic_accounting') diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index 4cb43356441..65b65e27c7f 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -30,7 +30,6 @@ up a management by affair. 'views/res_config_settings_views.xml', 'views/project_views.xml', 'views/project_portal_templates.xml', - 'views/project_update_templates.xml', 'views/hr_timesheet_portal_templates.xml', 'report/hr_timesheet_report_view.xml', 'report/project_report_view.xml', diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py index efa0ce7911a..dd55d6e5b98 100644 --- a/addons/hr_timesheet/models/__init__.py +++ b/addons/hr_timesheet/models/__init__.py @@ -9,5 +9,4 @@ from . import res_company from . import res_config_settings from . import project from . import project_collaborator -from . import project_update from . import uom diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 96d41250b3f..9c334067746 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -174,11 +174,48 @@ class Project(models.Model): warning_msg, self.env.ref('hr_timesheet.timesheet_action_project').id, _('See timesheet entries'), {'active_ids': projects_with_timesheets.ids}) + def action_show_timesheets_by_employee_invoice_type(self): + action = self.env["ir.actions.actions"]._for_xml_id("hr_timesheet.timesheet_action_all") + #Let's put the chart view first + new_views = [] + for view in action['views']: + new_views.insert(0, view) if view[1] == 'graph' else new_views.append(view) + action.update({ + 'display_name': _("Timesheets"), + 'domain': [('project_id', '=', self.id)], + 'context': { + 'default_project_id': self.id, + 'search_default_groupby_employee': True, + 'search_default_groupby_timesheet_invoice_type': True + }, + 'views': new_views + }) + + return action + def _convert_project_uom_to_timesheet_encode_uom(self, time): uom_from = self.company_id.project_time_mode_id uom_to = self.env.company.timesheet_encode_uom_id return round(uom_from._compute_quantity(time, uom_to, raise_if_failure=False), 2) + # ---------------------------- + # Project Updates + # ---------------------------- + + def _get_stat_buttons(self): + buttons = super(Project, self)._get_stat_buttons() + buttons.append({ + 'icon': 'clock-o', + 'text': _('Recorded'), + 'number': '%s %s' % (self.total_timesheet_time, self.env.company.timesheet_encode_uom_id.name), + 'action_type': 'object', + 'action': 'action_show_timesheets_by_employee_invoice_type', + 'show': self.user_has_groups('hr_timesheet.group_hr_timesheet_user') and self.allow_timesheets, + 'sequence': 4, + }) + return buttons + + class Task(models.Model): _name = "project.task" _inherit = "project.task" diff --git a/addons/hr_timesheet/models/project_update.py b/addons/hr_timesheet/models/project_update.py deleted file mode 100644 index c679c6cd66c..00000000000 --- a/addons/hr_timesheet/models/project_update.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from collections import defaultdict - -from odoo import api, fields, models -from odoo.tools.misc import formatLang - -class ProjectUpdate(models.Model): - _inherit = 'project.update' - - @api.model - def _get_template_values(self, project): - return { - **super(ProjectUpdate, self)._get_template_values(project), - 'people': self._get_people_values(project), - } - - @api.model - def _get_people_values(self, project): - return { - 'uom': self.env.company._timesheet_uom_text(), - 'is_uom_hour': self.env.company._is_timesheet_hour_uom(), - 'activities': self._get_activities(project) - } - - @api.model - def _get_activities(self, project): - if not self.user_has_groups('hr_timesheet.group_hr_timesheet_user'): - return [] - today = fields.Date.context_today(self) - query = """ - SELECT timesheet.employee_id as employee_id, - gs::date as period, - sum(timesheet.unit_amount) as unit_amount, - employee.name as name - FROM project_project p - INNER JOIN account_analytic_line timesheet - ON timesheet.project_id = p.id - INNER JOIN hr_employee employee - ON timesheet.employee_id = employee.id - CROSS JOIN generate_series( - %(today)s - '180 days'::interval, - %(today)s, - '30 days'::interval - ) gs - WHERE p.id = %(project_id)s - AND gs >= timesheet.date - AND gs - '30 days'::interval < timesheet.date - GROUP BY timesheet.employee_id, - gs, employee.name - ORDER BY gs DESC, employee.name ASC - """ - self.env.cr.execute(query, {'project_id': project.id, 'today': today}) - results = self.env.cr.dictfetchall() - activities = defaultdict(lambda: { - 'unit_amount': 0, - 'worked': False, - }) - digits = not self.env.company._is_timesheet_hour_uom() and 2 or 0 - for result in results: - if result['period'] == today: - activities[result['employee_id']] = { - 'name': result['name'], - 'unit_amount': formatLang(self.env, project._convert_project_uom_to_timesheet_encode_uom(result['unit_amount']), digits=digits), - 'worked': True, - 'new': True, - } - else: - name = activities[result['employee_id']].get('name', result['name']) - activities[result['employee_id']].update(name=name, new=False) - return list(activities.values()) diff --git a/addons/hr_timesheet/tests/__init__.py b/addons/hr_timesheet/tests/__init__.py index 47b3af52e56..2562aceb885 100644 --- a/addons/hr_timesheet/tests/__init__.py +++ b/addons/hr_timesheet/tests/__init__.py @@ -2,4 +2,3 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_timesheet -from . import test_project_update diff --git a/addons/hr_timesheet/tests/test_project_update.py b/addons/hr_timesheet/tests/test_project_update.py deleted file mode 100644 index 76c282536d4..00000000000 --- a/addons/hr_timesheet/tests/test_project_update.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo.tests import tagged -from odoo.tests.common import Form - -from odoo.addons.project.tests.test_project_update_flow import TestProjectUpdate - -@tagged('-at_install', 'post_install') -class TestProjectUpdateHrTimesheet(TestProjectUpdate): - - def _create_timesheet(self): - self.employee = self.env['hr.employee'].create({ - 'name': "Randy Heart", - 'company_id': self.env.company.id - }) - self.timesheet = self.env['account.analytic.line'].create({ - 'project_id': self.project_pigs.id, - 'task_id': self.task_1.id, - 'name': 'my first timesheet', - 'unit_amount': 4, - 'employee_id': self.employee.id, - }) - - def test_project_update_form_people(self): - self._create_timesheet() - try: - with Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id})) as update_form: - update_form.name = "Test" - update_form.progress = 65 - update = update_form.save() - except Exception as e: - raise AssertionError("Error raised unexpectedly while filling the project update form ! Exception : " + e.args[0]) - - self.assertTrue("People" in update.description, "The description should contain 'People'.") - - def test_project_update_form_no_people(self): - try: - with Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id})) as update_form: - update_form.name = "Test" - update_form.progress = 65 - update = update_form.save() - except Exception as e: - raise AssertionError("Error raised unexpectedly while filling the project update form ! Exception : " + e.args[0]) - - self.assertFalse("People" in update.description, "The description should not contain 'People'.") - - def test_project_update_description_people(self): - self._create_timesheet() - template_values = self.env['project.update']._get_template_values(self.project_pigs) - - self.assertEqual(template_values['people']['uom'], "hours", "The timesheet uom is well given to template") - self.assertTrue(template_values['people']['is_uom_hour'], "Company default timesheet uom is hours") - self.assertEqual(len(template_values['people']['activities']), 1, "The number of recorded timesheet activities is 1") - self.assertEqual(template_values['people']['activities'][0]['name'], "Randy Heart", "The number of recorded timesheet activities is 1") diff --git a/addons/hr_timesheet/views/project_update_templates.xml b/addons/hr_timesheet/views/project_update_templates.xml deleted file mode 100644 index 08a68b0c782..00000000000 --- a/addons/hr_timesheet/views/project_update_templates.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 56e68eb062a..5f83092af01 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -37,7 +37,7 @@ 24 - + @@ -102,11 +104,12 @@ Component="FormViewDialog" params="{ context: context, + _createContext: _createContext, on_saved: _onDialogSaved, res_id: false, res_model: 'project.milestone', title: NEW_PROJECT_MILESTONE, - disable_multiple_selection: true, + multi_select: true, }" t-ref="dialog" /> @@ -120,8 +123,10 @@
- - + + + +
diff --git a/addons/project/static/tests/tours/project_update_tour_tests.js b/addons/project/static/tests/tours/project_update_tour_tests.js index 5227a4e960b..ae9c3fe0ccf 100644 --- a/addons/project/static/tests/tours/project_update_tour_tests.js +++ b/addons/project/static/tests/tours/project_update_tour_tests.js @@ -73,9 +73,93 @@ tour.register('project_update_tour', { extra_trigger: '.o_kanban_project_tasks', run: "drag_and_drop .o_kanban_group:eq(1) ", }, { + trigger: '.o_back_button', + content: 'Go back to edit the project created', +}, { + // an invisible element cannot be used as a trigger so this small hack is mandatory for the next step + trigger: '.o_kanban_record:contains("New Project")', + run: function () { + $('.o_kanban_record:contains("New Project") .o_dropdown_kanban').css('visibility', 'visible'); + } +}, { + trigger: '.oe_kanban_global_click :contains("New Project") .o_dropdown_kanban', + content: 'Open the project dropdown' +}, { + trigger: '.o_kanban_record:contains("New Project") .dropdown-menu a:contains("See More")', + content: 'Start editing the project', +}, { + trigger: '.nav-item:contains("Invoicing") .nav-link', + content: 'Start the assignement of a default sale order id to the project' +}, { + trigger: '.o_form_button_edit', + content: 'Start editing the project' +}, { + trigger: 'div[name="sale_line_id"]', + content: 'Select the first sale order of the list', + run: function (actions) { + actions.text('Prepaid', this.$anchor.find('input')); + }, +}, { + trigger: 'ul.ui-autocomplete > li:first-child > a', + content: 'Select the first item on the autocomplete dropdown' +}, { + trigger: '.o_form_button_save', + content: 'Save the modifications' +}, { + trigger: '.o_back_button', + content: 'Go back to the kanban view the project created', +}, { + trigger: '.oe_kanban_global_click :contains("New Project")', + content: 'Open the project' +}, { trigger: ".o_project_updates_breadcrumb", + content: 'Open Updates' +}, { + trigger: ".oe_button_box .o_stat_text:contains('Sales Order')", + run: function () {} +}, { + trigger: ".o_rightpanel_title:eq(0):contains('Sold')", + content: 'Check the user sees Sold section', + run: function () {} +}, { + trigger: ".o_rightpanel_left_text:eq(0):contains('Prepaid Hours')", + content: 'Check the user sees Sold subsection', + run: function () {}, +}, { + trigger: ".o_rightpanel_title:eq(1):contains('Total Sold')", + content: 'Check the user sees Total Sold section', + run: function () {} +}, { + trigger: ".o_rightpanel_header:eq(1) .o_rightpanel_right_col:contains('Hours')", + content: 'Check the user sees Total Sold hours title', + run: function () {}, +}, { + trigger: ".o_rightpanel_section:eq(2) .o_rightpanel_data_row:contains('Effective')", + content: 'Check the user sees Total Sold subsection row', + run: function () {}, +}, { + trigger: ".o_rightpanel_section:eq(2) .o_rightpanel_data_row:contains('Remaining')", + content: 'Check the user sees Total Sold subsection row', + run: function () {}, +}, { + trigger: ".o_rightpanel_title:eq(2):contains('Profitability')", + content: 'Check the user sees Profitability section', + run: function () {} +}, { + trigger: ".o_rightpanel_section:eq(3) .o_rightpanel_data_row:contains('Revenues')", + content: 'Check the user sees Profitability subsection row', + run: function () {}, +}, { + trigger: ".o_rightpanel_section:eq(3) .o_rightpanel_data_row:contains('Costs')", + content: 'Check the user sees Profitability subsection row', + run: function () {}, +}, { + trigger: ".o_rightpanel_section:eq(3) .o_rightpanel_data_row:contains('Margin')", + content: 'Check the user sees Profitability subsection row', + run: function () {}, }, { trigger: ".o_add_milestone a", + content: "Add a first milestone" }, { trigger: "input.o_field_widget[name=name]", run: 'text New milestone' @@ -91,22 +175,13 @@ tour.register('project_update_tour', { run: 'text Second milestone' }, { trigger: "input.datetimepicker-input[name=deadline]", - run: 'text 12/12/2021' + run: 'text 12/12/2022' }, { trigger: ".modal-footer button" }, { - trigger: ".o_rightpanel_left_button:eq(0)" -}, { - trigger: ".breadcrumb :contains('Tasks Analysis')", -}, { - trigger: ".o_back_button" -}, { - trigger: ".o_rightpanel_left_button:eq(1)" -}, { - trigger: ".breadcrumb :contains('Burndown Chart')", -}, { - trigger: ".o_back_button" -}, { + trigger: '.o_back_button', + content: 'Go back to edit to the list view', + trigger: ".o_open_milestone:eq(1) .o_milestone_detail span:eq(0)", extra_trigger: ".o_add_milestone a", run: function () { @@ -120,25 +195,35 @@ tour.register('project_update_tour', { }, { trigger: ".modal-footer button" }, { - trigger: ".o-kanban-button-new" + trigger: ".o-kanban-button-new", + content: "Create a new update" }, { trigger: "input.o_field_widget[name=name]", run: 'text New update' }, { trigger: ".o_form_button_save" }, { - trigger: ".o_field_widget[name=description] :contains('1 tasks are open, out of 2.')", + trigger: ".o_field_widget[name=description] h1:contains('Activities')", run: function () {}, }, { - trigger: ".o_field_widget[name=description] :contains('2 were created')", + trigger: ".o_field_widget[name=description] h3:contains('Sold')", + run: function () {}, + }, { + trigger: ".o_field_widget[name=description] td:contains('Customer Care')", run: function () {}, }, { - trigger: ".o_field_widget[name=description] :contains('1 was closed')", + trigger: ".o_field_widget[name=description] h3:contains('Profitability')", run: function () {}, }, { - trigger: ".o_field_widget[name=description] :contains('New milestone') :contains('(due on 12/12/2100)')", + trigger: ".o_field_widget[name=description] h3:contains('Milestones')", run: function () {}, }, { - trigger: ".o_field_widget[name=description] :contains('New milestone') :contains('(12/12/2099 => 12/12/2100)')", + trigger: ".o_field_widget[name=description] div[name='milestone'] ul li:contains('(12/12/2099 => 12/12/2100)')", + run: function () {}, +}, { + trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due on 12/12/2022)')", + run: function () {}, +}, { + trigger: ".o_field_widget[name=description] div[name='milestone'] ul li span:contains('(due on 12/12/2100)')", run: function () {}, }]); diff --git a/addons/project/tests/test_project_update_flow.py b/addons/project/tests/test_project_update_flow.py index 18179a572dd..4435f265197 100644 --- a/addons/project/tests/test_project_update_flow.py +++ b/addons/project/tests/test_project_update_flow.py @@ -26,8 +26,7 @@ class TestProjectUpdate(TestProjectCommon): self.assertEqual(update.user_id, self.env.user, "The author is the user who created the update.") self.assertNotEqual(len(update.description), 0, "The description should not be empty.") - self.assertTrue("Tasks" in update.description, "The description should contain 'Tasks'.") - self.assertTrue("Milestone" in update.description, "The description should contain 'Milestone'.") + self.assertTrue("Activities" in update.description, "The description should contain 'Activities'.") self.assertEqual(update.status, 'on_track', "The status should be the default one.") with Form(self.env['project.update'].with_context({'default_project_id': self.project_pigs.id})) as update_form: @@ -48,11 +47,6 @@ class TestProjectUpdate(TestProjectCommon): template_values = self.env['project.update']._get_template_values(self.project_pigs) - self.assertEqual(template_values['tasks']['open_tasks'], 2, "Open tasks should be equal to 2") - self.assertEqual(template_values['tasks']['total_tasks'], 2, "Total tasks should be equal to 2") - self.assertEqual(template_values['tasks']['created_tasks'], 2, "Created tasks should be equal to 2") - self.assertEqual(template_values['tasks']['closed_tasks'], 0, "Closed tasks should be equal to 0") - self.assertEqual(len(template_values['milestones']['list']), 2, "Milestone list length should be equal to 2") self.assertEqual(len(template_values['milestones']['created']), 3, "Milestone created length tasks should be equal to 3") @@ -70,7 +64,6 @@ class TestProjectUpdate(TestProjectCommon): panel_data = self.project_pigs.get_panel_data() - self.assertEqual(len(panel_data['tasks_analysis']['data']), 2, "Panel data should contain 'tasks' entry") self.assertEqual(len(panel_data['milestones']['data']), 3, "Panel data should contain 'milestone' entry") self.assertFalse(panel_data['milestones']['data'][0]['is_deadline_exceeded'], "Milestone is achieved") self.assertTrue(panel_data['milestones']['data'][1]['is_deadline_exceeded'], "Milestone is exceeded") diff --git a/addons/project/views/project_update_templates.xml b/addons/project/views/project_update_templates.xml index 459b1e56ec6..42a21c0565a 100644 --- a/addons/project/views/project_update_templates.xml +++ b/addons/project/views/project_update_templates.xml @@ -10,23 +10,19 @@