From 43fa3f309c95d7b34232f15ebf88b873549dea83 Mon Sep 17 00:00:00 2001 From: tarteo Date: Thu, 12 Sep 2024 09:56:20 +0200 Subject: [PATCH] [REF] Move account_invoice_hour_report, account_invoice_hour_report_non_billable, hr_holidays_resource, sale_timesheet_approval, sale_timesheet_approval_non_billable, sale_timesheet_approval_sheet to onesteinbv/addons-generic [REM] accountancy_install: Not used anymore --- account_invoice_hour_report/README.rst | 7 + account_invoice_hour_report/__init__.py | 3 + account_invoice_hour_report/__manifest__.py | 21 ++ account_invoice_hour_report/i18n/nl.po | 93 ++++++++ .../models/__init__.py | 4 + .../models/res_partner.py | 10 + account_invoice_hour_report/models/sale.py | 35 +++ .../templates/hour_report_template.xml | 119 ++++++++++ .../views/hours_report.xml | 12 + .../views/res_partner.xml | 14 ++ .../README.rst | 7 + .../__init__.py | 0 .../__manifest__.py | 17 ++ .../templates/hour_report_template.xml | 13 ++ hr_holidays_resource/__init__.py | 1 + hr_holidays_resource/__manifest__.py | 16 ++ .../data/hr_holidays_data.xml | 22 ++ hr_holidays_resource/data/ir_cron_data.xml | 13 ++ hr_holidays_resource/models/__init__.py | 1 + hr_holidays_resource/models/resource.py | 208 ++++++++++++++++++ sale_timesheet_approval/README.rst | 32 +++ sale_timesheet_approval/__init__.py | 5 + sale_timesheet_approval/__manifest__.py | 23 ++ .../data/account_analytic_line_data.xml | 16 ++ sale_timesheet_approval/i18n/nl.po | 116 ++++++++++ sale_timesheet_approval/models/__init__.py | 5 + .../models/account_analytic_line.py | 18 ++ .../models/account_move_line.py | 18 ++ .../models/sale_order_line.py | 63 ++++++ sale_timesheet_approval/post_install.py | 7 + .../security/ir.model.access.csv | 2 + .../views/hr_timesheet_views.xml | 42 ++++ sale_timesheet_approval/wizard/__init__.py | 4 + .../wizard/hr_timesheet_invoice_create.py | 43 ++++ .../hr_timesheet_invoice_create_view.xml | 33 +++ .../wizard/sale_make_invoice_advance.py | 29 +++ .../README.rst | 11 + .../__init__.py | 3 + .../__manifest__.py | 16 ++ .../wizard/__init__.py | 3 + .../wizard/hr_timesheet_invoice_create.py | 19 ++ sale_timesheet_approval_sheet/README.rst | 17 ++ sale_timesheet_approval_sheet/__init__.py | 0 sale_timesheet_approval_sheet/__manifest__.py | 21 ++ .../views/hr_timesheet_views.xml | 20 ++ 45 files changed, 1182 insertions(+) create mode 100644 account_invoice_hour_report/README.rst create mode 100644 account_invoice_hour_report/__init__.py create mode 100644 account_invoice_hour_report/__manifest__.py create mode 100644 account_invoice_hour_report/i18n/nl.po create mode 100644 account_invoice_hour_report/models/__init__.py create mode 100644 account_invoice_hour_report/models/res_partner.py create mode 100644 account_invoice_hour_report/models/sale.py create mode 100644 account_invoice_hour_report/templates/hour_report_template.xml create mode 100644 account_invoice_hour_report/views/hours_report.xml create mode 100644 account_invoice_hour_report/views/res_partner.xml create mode 100644 account_invoice_hour_report_non_billable/README.rst create mode 100644 account_invoice_hour_report_non_billable/__init__.py create mode 100644 account_invoice_hour_report_non_billable/__manifest__.py create mode 100644 account_invoice_hour_report_non_billable/templates/hour_report_template.xml create mode 100644 hr_holidays_resource/__init__.py create mode 100644 hr_holidays_resource/__manifest__.py create mode 100644 hr_holidays_resource/data/hr_holidays_data.xml create mode 100644 hr_holidays_resource/data/ir_cron_data.xml create mode 100644 hr_holidays_resource/models/__init__.py create mode 100644 hr_holidays_resource/models/resource.py create mode 100644 sale_timesheet_approval/README.rst create mode 100644 sale_timesheet_approval/__init__.py create mode 100644 sale_timesheet_approval/__manifest__.py create mode 100644 sale_timesheet_approval/data/account_analytic_line_data.xml create mode 100644 sale_timesheet_approval/i18n/nl.po create mode 100644 sale_timesheet_approval/models/__init__.py create mode 100644 sale_timesheet_approval/models/account_analytic_line.py create mode 100644 sale_timesheet_approval/models/account_move_line.py create mode 100644 sale_timesheet_approval/models/sale_order_line.py create mode 100644 sale_timesheet_approval/post_install.py create mode 100644 sale_timesheet_approval/security/ir.model.access.csv create mode 100644 sale_timesheet_approval/views/hr_timesheet_views.xml create mode 100644 sale_timesheet_approval/wizard/__init__.py create mode 100644 sale_timesheet_approval/wizard/hr_timesheet_invoice_create.py create mode 100644 sale_timesheet_approval/wizard/hr_timesheet_invoice_create_view.xml create mode 100644 sale_timesheet_approval/wizard/sale_make_invoice_advance.py create mode 100644 sale_timesheet_approval_non_billable/README.rst create mode 100644 sale_timesheet_approval_non_billable/__init__.py create mode 100644 sale_timesheet_approval_non_billable/__manifest__.py create mode 100644 sale_timesheet_approval_non_billable/wizard/__init__.py create mode 100644 sale_timesheet_approval_non_billable/wizard/hr_timesheet_invoice_create.py create mode 100644 sale_timesheet_approval_sheet/README.rst create mode 100644 sale_timesheet_approval_sheet/__init__.py create mode 100644 sale_timesheet_approval_sheet/__manifest__.py create mode 100644 sale_timesheet_approval_sheet/views/hr_timesheet_views.xml diff --git a/account_invoice_hour_report/README.rst b/account_invoice_hour_report/README.rst new file mode 100644 index 0000000..d5939c4 --- /dev/null +++ b/account_invoice_hour_report/README.rst @@ -0,0 +1,7 @@ + + +================================= +Hour report addition for invoices +================================= + +This module attaches an annex consisting of the related work hours made by employees to the invoiced tasks. diff --git a/account_invoice_hour_report/__init__.py b/account_invoice_hour_report/__init__.py new file mode 100644 index 0000000..31660d6 --- /dev/null +++ b/account_invoice_hour_report/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/account_invoice_hour_report/__manifest__.py b/account_invoice_hour_report/__manifest__.py new file mode 100644 index 0000000..a072baa --- /dev/null +++ b/account_invoice_hour_report/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Invoice hour report addition", + "version": "16.0.1.0.0", + "author": "Onestein", + "category": "Accounting & Finance", + "website": "https://www.onestein.nl", + "license": "AGPL-3", + "depends": [ + "sale_timesheet_approval", + "sale_timesheet_custom_fields", + ], + "data": [ + "templates/hour_report_template.xml", + "views/hours_report.xml", + "views/res_partner.xml", + ], + "installable": True, +} diff --git a/account_invoice_hour_report/i18n/nl.po b/account_invoice_hour_report/i18n/nl.po new file mode 100644 index 0000000..b7af557 --- /dev/null +++ b/account_invoice_hour_report/i18n/nl.po @@ -0,0 +1,93 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_invoice_hour_report +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-26 12:47+0000\n" +"PO-Revision-Date: 2024-03-26 12:47+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Invoice Annex" +msgstr "Factuurbijlage" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Customer Name:" +msgstr "Klantnaam:" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Sale Order:" +msgstr "Verkooporder: +" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Total:" +msgstr "Totaal:" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Date" +msgstr "Datum" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Description" +msgstr "Beschrijving" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Employee" +msgstr "" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Project:" +msgstr "Werknemer" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Time to be Invoiced (Hours)" +msgstr "Tijd om te factureren (uren)" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Total Hours" +msgstr "Totaal aantal uren" + +#. module: account_invoice_hour_report +#: model_terms:ir.ui.view,arch_db:account_invoice_hour_report.invoice_hours_report_document +msgid "Week nr." +msgstr "Weeknr." + +#. module: account_invoice_hour_report +#: model:ir.model,name:account_invoice_hour_report.model_res_partner +msgid "Contact" +msgstr "" + +#. module: account_invoice_hour_report +#: model:ir.model.fields,field_description:account_invoice_hour_report.field_res_partner__print_timesheet_employee +#: model:ir.model.fields,field_description:account_invoice_hour_report.field_res_users__print_timesheet_employee +msgid "Print Employee on Timesheet Report" +msgstr "Werknemer afdrukken op urenstaatrapport" + +#. module: account_invoice_hour_report +#: model:ir.model,name:account_invoice_hour_report.model_sale_order +msgid "Sales Order" +msgstr "Verkooporder" + +#. module: account_invoice_hour_report +#: model:ir.actions.report,name:account_invoice_hour_report.report_invoice_hours_report +msgid "Timesheets" +msgstr "Uren rapport" diff --git a/account_invoice_hour_report/models/__init__.py b/account_invoice_hour_report/models/__init__.py new file mode 100644 index 0000000..49f4403 --- /dev/null +++ b/account_invoice_hour_report/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import sale +from . import res_partner diff --git a/account_invoice_hour_report/models/res_partner.py b/account_invoice_hour_report/models/res_partner.py new file mode 100644 index 0000000..8dfa933 --- /dev/null +++ b/account_invoice_hour_report/models/res_partner.py @@ -0,0 +1,10 @@ +# Copyright 2024 Onestein () + + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + print_timesheet_employee = fields.Boolean("Print Employee on Timesheet Report") diff --git a/account_invoice_hour_report/models/sale.py b/account_invoice_hour_report/models/sale.py new file mode 100644 index 0000000..1be18b7 --- /dev/null +++ b/account_invoice_hour_report/models/sale.py @@ -0,0 +1,35 @@ +# Copyright 2024 Onestein () + +import base64 + +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def _create_invoices(self, grouped=False, final=False, date=None): + res = super()._create_invoices(grouped=grouped, final=final, date=date) + report_obj = self.env["ir.actions.report"] + for move in res: + ctx = { + "tz": self.env.user.tz, + "uid": self.env.uid, + "lang": move.partner_id.lang, + } + timesheet_lines = move.timesheet_ids + if timesheet_lines: + pdf, _ = report_obj.with_context(ctx)._render_qweb_pdf( + "account_invoice_hour_report.report_invoice_hours_report", + res_ids=timesheet_lines.ids, + ) + self.env["ir.attachment"].create( + { + "name": "timesheet_detail.pdf", + "res_id": move.id, + "res_model": str(move._name), + "datas": base64.b64encode(pdf), + "mimetype": "application/pdf", + } + ) + return res diff --git a/account_invoice_hour_report/templates/hour_report_template.xml b/account_invoice_hour_report/templates/hour_report_template.xml new file mode 100644 index 0000000..3ac88e8 --- /dev/null +++ b/account_invoice_hour_report/templates/hour_report_template.xml @@ -0,0 +1,119 @@ + + + + + + diff --git a/account_invoice_hour_report/views/hours_report.xml b/account_invoice_hour_report/views/hours_report.xml new file mode 100644 index 0000000..25c712b --- /dev/null +++ b/account_invoice_hour_report/views/hours_report.xml @@ -0,0 +1,12 @@ + + + + Hours To Be Invoiced Report + account.analytic.line + qweb-pdf + account_invoice_hour_report.invoice_hours_report_document + account_invoice_hour_report.invoice_hours_report_document + + report + + diff --git a/account_invoice_hour_report/views/res_partner.xml b/account_invoice_hour_report/views/res_partner.xml new file mode 100644 index 0000000..636bae0 --- /dev/null +++ b/account_invoice_hour_report/views/res_partner.xml @@ -0,0 +1,14 @@ + + + + + res.partner + + + + + + + + + diff --git a/account_invoice_hour_report_non_billable/README.rst b/account_invoice_hour_report_non_billable/README.rst new file mode 100644 index 0000000..995cc7e --- /dev/null +++ b/account_invoice_hour_report_non_billable/README.rst @@ -0,0 +1,7 @@ + + +============================================================== +Hour report addition With Non Billable Timesheets for invoices +============================================================== + +This module is the bridge between account_invoice_hour_report and sale_timesheet_line_non_billable module. diff --git a/account_invoice_hour_report_non_billable/__init__.py b/account_invoice_hour_report_non_billable/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/account_invoice_hour_report_non_billable/__manifest__.py b/account_invoice_hour_report_non_billable/__manifest__.py new file mode 100644 index 0000000..3aa24ba --- /dev/null +++ b/account_invoice_hour_report_non_billable/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Invoice Hour Report With Non Billable Timesheets", + "version": "16.0.1.0.0", + "author": "Onestein", + "license": "AGPL-3", + "category": "Accounting & Finance", + "website": "https://www.onestein.nl", + "depends": ["account_invoice_hour_report", "sale_timesheet_line_non_billable"], + "data": [ + "templates/hour_report_template.xml", + ], + "installable": True, + "auto_install": True, +} diff --git a/account_invoice_hour_report_non_billable/templates/hour_report_template.xml b/account_invoice_hour_report_non_billable/templates/hour_report_template.xml new file mode 100644 index 0000000..9dd1da0 --- /dev/null +++ b/account_invoice_hour_report_non_billable/templates/hour_report_template.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/hr_holidays_resource/__init__.py b/hr_holidays_resource/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/hr_holidays_resource/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_resource/__manifest__.py b/hr_holidays_resource/__manifest__.py new file mode 100644 index 0000000..15e84b9 --- /dev/null +++ b/hr_holidays_resource/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Time Off Resource", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "category": "Productivity/Calendar", + "author": "Onestein", + "website": "https://www.onestein.nl", + "depends": ["hr_holidays"], + "data": [ + "data/hr_holidays_data.xml", + "data/ir_cron_data.xml", + ], +} diff --git a/hr_holidays_resource/data/hr_holidays_data.xml b/hr_holidays_resource/data/hr_holidays_data.xml new file mode 100644 index 0000000..b89bdb1 --- /dev/null +++ b/hr_holidays_resource/data/hr_holidays_data.xml @@ -0,0 +1,22 @@ + + + + + + Contract Time Off + no + no_validation + + 10 + + + + Contract Time Off Partial + no + no_validation + + 10 + half_day + + + diff --git a/hr_holidays_resource/data/ir_cron_data.xml b/hr_holidays_resource/data/ir_cron_data.xml new file mode 100644 index 0000000..7645c26 --- /dev/null +++ b/hr_holidays_resource/data/ir_cron_data.xml @@ -0,0 +1,13 @@ + + + + Contract Time Off: Updates non and partially working days for contracted resources + + code + model._update_contract_time_off() + 1 + days + -1 + + + diff --git a/hr_holidays_resource/models/__init__.py b/hr_holidays_resource/models/__init__.py new file mode 100644 index 0000000..364a06e --- /dev/null +++ b/hr_holidays_resource/models/__init__.py @@ -0,0 +1 @@ +from . import resource diff --git a/hr_holidays_resource/models/resource.py b/hr_holidays_resource/models/resource.py new file mode 100644 index 0000000..b7a4063 --- /dev/null +++ b/hr_holidays_resource/models/resource.py @@ -0,0 +1,208 @@ +from collections import namedtuple +from datetime import timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + +from odoo.addons.resource.models.resource import float_to_time + +DummyAttendance = namedtuple( + "DummyAttendance", "hour_from, hour_to, dayofweek, day_period, week_type" +) + + +def find_all_dates_for_day_of_week_between_range(start, end, weekday): + total_days = (end - start).days + all_days = [start + timedelta(days=day) for day in range(total_days)] + return [day for day in all_days if day.weekday() == int(weekday)] + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + @api.model + def _update_contract_time_off(self): # noqa: disable=C901 + """ + Method called by the cron task in order to update the contract + time off for resources. + """ + today = fields.Datetime.today() + three_months_from_today = today + relativedelta(months=3) + resource_calendars = self.search([("two_weeks_calendar", "=", False)]) + resource_obj = self.env["resource.resource"] + hr_leave_obj = self.env["hr.leave"] + contract_time_off_holiday_status_id = self.env.ref( + "hr_holidays_resource.holiday_status_cto", raise_if_not_found=False + ) + contract_time_off_partial_holiday_status_id = self.env.ref( + "hr_holidays_resource.holiday_status_ctop", raise_if_not_found=False + ) + for resource_calendar in resource_calendars: + non_working_periods = {} + for day in ["0", "1", "2", "3", "4"]: + working_periods = resource_calendar.attendance_ids.filtered( + lambda r: r.dayofweek == day + ) + if not working_periods: + non_working_periods.update({day: "full day"}) + continue + if not working_periods.filtered(lambda wp: wp.day_period == "morning"): + non_working_periods.update({day: "morning"}) + continue + if not working_periods.filtered( + lambda wp: wp.day_period == "afternoon" + ): + non_working_periods.update({day: "afternoon"}) + continue + if non_working_periods: + attendances = self._get_attendances(resource_calendar.id) + for day, period in non_working_periods.items(): + all_non_working_dates = ( + find_all_dates_for_day_of_week_between_range( + today, three_months_from_today, day + ) + ) + if period == "full day": + if contract_time_off_holiday_status_id: + for resource in resource_obj.search( + [ + ("calendar_id", "=", resource_calendar.id), + ("resource_type", "=", "user"), + ] + ): + for non_working_date in all_non_working_dates: + try: + with self.env.cr.savepoint(): + hr_leave_obj.create( + self.prepare_hr_leave_vals( + non_working_date, + contract_time_off_holiday_status_id, + resource, + attendances, + ) + ) + except Exception: + continue + else: + if contract_time_off_partial_holiday_status_id: + for resource in resource_obj.search( + [ + ("calendar_id", "=", resource_calendar.id), + ("resource_type", "=", "user"), + ] + ): + for non_working_date in all_non_working_dates: + try: + with self.env.cr.savepoint(): + hr_leave_obj.create( + self.prepare_hr_leave_vals( + non_working_date, + contract_time_off_holiday_status_id, + resource, + attendances, + period, + ) + ) + except Exception: + continue + + def prepare_hr_leave_vals( + self, date, holiday_status_id, resource, attendances, period=False + ): + employee = resource.employee_id[0] + vals = { + "holiday_status_id": holiday_status_id.id, + "employee_id": employee.id, + "number_of_days": 1, + } + default_value = DummyAttendance(0, 0, 0, "morning", False) + if period: + # find first attendance coming after first_day for the specified period + attendance_from = next( + (att for att in attendances if att.day_period == period), + attendances[0] if attendances else default_value, + ) + # find last attendance coming before last_day for the specified period + attendance_to = next( + (att for att in reversed(attendances) if att.day_period == period), + attendances[-1] if attendances else default_value, + ) + else: + # find first attendance coming after first_day + attendance_from = next( + (att for att in attendances if int(att.dayofweek) >= date.weekday()), + attendances[0] if attendances else default_value, + ) + # find last attendance coming before last_day + attendance_to = next( + ( + att + for att in reversed(attendances) + if int(att.dayofweek) <= date.weekday() + ), + attendances[-1] if attendances else default_value, + ) + hour_from = float_to_time(attendance_from.hour_from) + hour_to = float_to_time(attendance_to.hour_to) + hour_from = hour_from.hour + hour_from.minute / 60 + hour_to = hour_to.hour + hour_to.minute / 60 + + vals["date_from"] = self.env["hr.leave"]._get_start_or_end_from_attendance( + hour_from, date.date(), employee + ) + vals["date_to"] = self.env["hr.leave"]._get_start_or_end_from_attendance( + hour_to, date.date(), employee + ) + vals["request_date_from"], vals["request_date_to"] = ( + vals["date_from"].date(), + vals["date_to"].date(), + ) + if period: + vals.update( + { + "number_of_days": 0.5, + "request_date_from_period": "am" if period == "morning" else "pm", + "request_unit_half": True, + "request_date_from": vals["date_from"], + "request_date_to": vals["date_to"], + } + ) + else: + vals.update({"number_of_days": 1}) + return vals + + def _get_attendances(self, resource_calendar_id): + domain = [ + ("calendar_id", "=", resource_calendar_id), + ("display_type", "=", False), + ] + attendances = self.env["resource.calendar.attendance"].read_group( + domain, + [ + "ids:array_agg(id)", + "hour_from:min(hour_from)", + "hour_to:max(hour_to)", + "week_type", + "dayofweek", + "day_period", + ], + ["week_type", "dayofweek", "day_period"], + lazy=False, + ) + + # Must be sorted by dayofweek ASC and day_period DESC + attendances = sorted( + [ + DummyAttendance( + group["hour_from"], + group["hour_to"], + group["dayofweek"], + group["day_period"], + group["week_type"], + ) + for group in attendances + ], + key=lambda att: (att.dayofweek, att.day_period != "morning"), + ) + return attendances diff --git a/sale_timesheet_approval/README.rst b/sale_timesheet_approval/README.rst new file mode 100644 index 0000000..d0ebfef --- /dev/null +++ b/sale_timesheet_approval/README.rst @@ -0,0 +1,32 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +================== +Timesheet Approval +================== + +In Odoo 8.0 you could invoice hours based on timesheet lines. +In later versions of Odoo this option is gone (all timesheet lines that are connected to a sales orderline will be invoiced immediately). + +This module allows to approve timesheet lines. Only timesheet lines that are approved can be invoiced. + +To be able to approve timesheet lines (and create invoices), the related timesheet must be in status 'approved' as well. +It is not possible to delete or edit timesheet lines AFTER they are validated: to invoice another amount of hours, the user must change that on the invoice line. + +NOTICE: this module could be conflicting with enterprise module timesheet_grid ! + +Known issues / Roadmap +====================== + + * Move "week_number" field and related filters to a separate module + +Credits +======= + +Contributors +------------ + +* Andrea Stirpe +* Antonio Esposito +* Anjeel Haria diff --git a/sale_timesheet_approval/__init__.py b/sale_timesheet_approval/__init__.py new file mode 100644 index 0000000..e8b78e5 --- /dev/null +++ b/sale_timesheet_approval/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard +from .post_install import set_is_approved diff --git a/sale_timesheet_approval/__manifest__.py b/sale_timesheet_approval/__manifest__.py new file mode 100644 index 0000000..72333c3 --- /dev/null +++ b/sale_timesheet_approval/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Timesheet Approval", + "version": "16.0.1.0.0", + "summary": "Timesheet Approval management", + "author": "Onestein", + "license": "AGPL-3", + "website": "https://www.onestein.nl", + "category": "Human Resources", + "depends": [ + "sale_timesheet", + ], + "data": [ + "security/ir.model.access.csv", + "data/account_analytic_line_data.xml", + "views/hr_timesheet_views.xml", + "wizard/hr_timesheet_invoice_create_view.xml", + ], + "post_init_hook": "set_is_approved", + "installable": True, +} diff --git a/sale_timesheet_approval/data/account_analytic_line_data.xml b/sale_timesheet_approval/data/account_analytic_line_data.xml new file mode 100644 index 0000000..205f5e3 --- /dev/null +++ b/sale_timesheet_approval/data/account_analytic_line_data.xml @@ -0,0 +1,16 @@ + + + + + Unapprove + + + list,form + code + + if records: + action = records.action_unapprove() + + + + diff --git a/sale_timesheet_approval/i18n/nl.po b/sale_timesheet_approval/i18n/nl.po new file mode 100644 index 0000000..5e8d65e --- /dev/null +++ b/sale_timesheet_approval/i18n/nl.po @@ -0,0 +1,116 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_timesheet_approval +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-26 12:39+0000\n" +"PO-Revision-Date: 2024-03-26 12:39+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_timesheet_approval +#: model:ir.model,name:sale_timesheet_approval.model_account_analytic_line +msgid "Analytic Line" +msgstr "Kostenplaatsregel" + +#. module: sale_timesheet_approval +#: model:ir.actions.act_window,name:sale_timesheet_approval.hr_timesheet_invoice_create_values +#: model_terms:ir.ui.view,arch_db:sale_timesheet_approval.view_hr_timesheet_invoice_create +msgid "Approve" +msgstr "Goedkeuren" + +#. module: sale_timesheet_approval +#: model_terms:ir.ui.view,arch_db:sale_timesheet_approval.view_hr_timesheet_invoice_create +msgid "Approve and Create Invoices" +msgstr "Facturen goedkeuren en maken" + +#. module: sale_timesheet_approval +#: model_terms:ir.ui.view,arch_db:sale_timesheet_approval.view_hr_timesheet_invoice_create +msgid "Cancel" +msgstr "Annuleren" + +#. module: sale_timesheet_approval +#: model:ir.model,name:sale_timesheet_approval.model_hr_timesheet_invoice_create +msgid "Create invoice from timesheet" +msgstr "Factuur maken op basis van urenstaat" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create__create_uid +msgid "Created by" +msgstr "Gemaakt door" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create__create_date +msgid "Created on" +msgstr "Gemaakt op" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create__display_name +msgid "Display Name" +msgstr "Weergavenaam" + +#. module: sale_timesheet_approval +#: model_terms:ir.ui.view,arch_db:sale_timesheet_approval.view_hr_timesheet_invoice_create +msgid "" +"Do you want to approve (and make invoiceable) the selected timesheet lines?" +msgstr "" +"Wilt u de geselecteerde urenstaatregels goedkeuren (en factureerbaar maken)?" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create__id +msgid "ID" +msgstr "" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_account_analytic_line__is_approved +msgid "Is Approved" +msgstr "Is goedgekeurd" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create____last_update +msgid "Last Modified on" +msgstr "Laatst gewijzigd op" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create__write_uid +msgid "Last Updated by" +msgstr "Laatst bijgewerkt door" + +#. module: sale_timesheet_approval +#: model:ir.model.fields,field_description:sale_timesheet_approval.field_hr_timesheet_invoice_create__write_date +msgid "Last Updated on" +msgstr "Laatst geupdate op" + +#. module: sale_timesheet_approval +#: model:ir.model,name:sale_timesheet_approval.model_sale_order_line +msgid "Sales Order Line" +msgstr "Verkooporderregel" + +#. module: sale_timesheet_approval +#: model:ir.actions.act_window,name:sale_timesheet_approval.timesheet_action_not_approved +msgid "Timesheets To Be Invoiced" +msgstr "Te factureren urenstaten" + +#. module: sale_timesheet_approval +#: model:ir.ui.menu,name:sale_timesheet_approval.timesheet_menu_activity_not_approved +msgid "To Be Invoiced" +msgstr "Gefactureerd worden" + +#. module: sale_timesheet_approval +#: model:ir.actions.server,name:sale_timesheet_approval.action_unapprove_timesheet_line +msgid "Unapprove" +msgstr "Goedkeuren intrekken" + +#. module: sale_timesheet_approval +#. odoo-python +#: code:addons/sale_timesheet_approval/models/account_analytic_line.py:0 +#, python-format +msgid "You cannot unapprove a timesheet line already invoiced" +msgstr "U kunt de goedkeuring van een urenstaatregel die al is gefactureerd, niet intrekken" diff --git a/sale_timesheet_approval/models/__init__.py b/sale_timesheet_approval/models/__init__.py new file mode 100644 index 0000000..3622c1c --- /dev/null +++ b/sale_timesheet_approval/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import account_analytic_line +from . import account_move_line +from . import sale_order_line diff --git a/sale_timesheet_approval/models/account_analytic_line.py b/sale_timesheet_approval/models/account_analytic_line.py new file mode 100644 index 0000000..f4affb0 --- /dev/null +++ b/sale_timesheet_approval/models/account_analytic_line.py @@ -0,0 +1,18 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + is_approved = fields.Boolean() + + def action_unapprove(self): + if self.filtered(lambda x: x.timesheet_invoice_id): + raise ValidationError( + _("You cannot unapprove a timesheet line already invoiced") + ) + self.write({"is_approved": False}) diff --git a/sale_timesheet_approval/models/account_move_line.py b/sale_timesheet_approval/models/account_move_line.py new file mode 100644 index 0000000..42f36e1 --- /dev/null +++ b/sale_timesheet_approval/models/account_move_line.py @@ -0,0 +1,18 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + @api.model + def _timesheet_domain_get_invoiced_lines(self, sale_line_delivery): + """Only the selected timesheets should be invoiced""" + domain = super(AccountMoveLine, self)._timesheet_domain_get_invoiced_lines( + sale_line_delivery=sale_line_delivery + ) + if self._context.get("timesheet_ids"): + domain = [("id", "in", self._context["timesheet_ids"])] + domain + return domain diff --git a/sale_timesheet_approval/models/sale_order_line.py b/sale_timesheet_approval/models/sale_order_line.py new file mode 100644 index 0000000..ab6fc87 --- /dev/null +++ b/sale_timesheet_approval/models/sale_order_line.py @@ -0,0 +1,63 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models +from odoo.osv import expression + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.depends("analytic_line_ids.is_approved") + def _compute_qty_delivered(self): + return super(SaleOrderLine, self)._compute_qty_delivered() + + def _timesheet_compute_delivered_quantity_domain(self): + """Non approved timesheets should not be considered""" + domain = super( + SaleOrderLine, self + )._timesheet_compute_delivered_quantity_domain() + domain += [("is_approved", "=", True)] + return domain + + def _recompute_qty_to_invoice_based_on_selected_timesheets(self): + """Recompute the qty_to_invoice field for product containing timesheets + + Search the existed timesheets as per the timesheets in context in parameter. + Retrieve the unit_amount of this timesheet and then recompute + the qty_to_invoice for each current product. + """ + lines_by_timesheet = self.filtered( + lambda sol: sol.product_id and sol.product_id._is_delivered_timesheet() + ) + domain = lines_by_timesheet._timesheet_compute_delivered_quantity_domain() + refund_account_moves = self.order_id.invoice_ids.filtered( + lambda am: am.state == "posted" and am.move_type == "out_refund" + ).reversed_entry_id + timesheet_domain = [ + "|", + ("timesheet_invoice_id", "=", False), + ("timesheet_invoice_id.state", "=", "cancel"), + ] + if refund_account_moves: + credited_timesheet_domain = [ + ("timesheet_invoice_id.state", "=", "posted"), + ("timesheet_invoice_id", "in", refund_account_moves.ids), + ] + timesheet_domain = expression.OR( + [timesheet_domain, credited_timesheet_domain] + ) + domain = expression.AND([domain, timesheet_domain]) + if self._context.get("timesheet_ids"): + domain = expression.AND( + [domain, [("id", "in", self._context["timesheet_ids"])]] + ) + mapping = lines_by_timesheet.sudo()._get_delivered_quantity_by_analytic(domain) + for line in lines_by_timesheet: + qty_to_invoice = mapping.get(line.id, 0.0) + if qty_to_invoice: + line.qty_to_invoice = qty_to_invoice + else: + prev_inv_status = line.invoice_status + line.qty_to_invoice = qty_to_invoice + line.invoice_status = prev_inv_status diff --git a/sale_timesheet_approval/post_install.py b/sale_timesheet_approval/post_install.py new file mode 100644 index 0000000..114a646 --- /dev/null +++ b/sale_timesheet_approval/post_install.py @@ -0,0 +1,7 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +def set_is_approved(cr, registry): + cr.execute("UPDATE account_analytic_line " "SET is_approved=true") + return diff --git a/sale_timesheet_approval/security/ir.model.access.csv b/sale_timesheet_approval/security/ir.model.access.csv new file mode 100644 index 0000000..1737b79 --- /dev/null +++ b/sale_timesheet_approval/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_timesheet_invoice_create,access.hr.timesheet.invoice.create,model_hr_timesheet_invoice_create,hr_timesheet.group_timesheet_manager,1,1,1,0 diff --git a/sale_timesheet_approval/views/hr_timesheet_views.xml b/sale_timesheet_approval/views/hr_timesheet_views.xml new file mode 100644 index 0000000..4a98b03 --- /dev/null +++ b/sale_timesheet_approval/views/hr_timesheet_views.xml @@ -0,0 +1,42 @@ + + + + account.analytic.line.tree + account.analytic.line + + + + + + + + + + + Timesheets To Be Invoiced + account.analytic.line + + [('project_id', '!=', False),('is_approved', '=', False),('timesheet_invoice_id','=',False)] + + + + + tree + + + + + + + form + + + + + + + diff --git a/sale_timesheet_approval/wizard/__init__.py b/sale_timesheet_approval/wizard/__init__.py new file mode 100644 index 0000000..20f9276 --- /dev/null +++ b/sale_timesheet_approval/wizard/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import hr_timesheet_invoice_create +from . import sale_make_invoice_advance diff --git a/sale_timesheet_approval/wizard/hr_timesheet_invoice_create.py b/sale_timesheet_approval/wizard/hr_timesheet_invoice_create.py new file mode 100644 index 0000000..be94e50 --- /dev/null +++ b/sale_timesheet_approval/wizard/hr_timesheet_invoice_create.py @@ -0,0 +1,43 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class HrTimesheetInvoiceCreate(models.TransientModel): + _name = "hr.timesheet.invoice.create" + _description = "Create invoice from timesheet" + + def do_approve(self): + self.ensure_one() + line_ids = self.env.context.get("active_ids") + lines = ( + self.env["account.analytic.line"] + .browse(line_ids) + .filtered(lambda x: not x.is_approved) + ) + lines.write({"is_approved": True}) + + def do_create(self): + self.ensure_one() + line_ids = self.env.context.get("active_ids") + lines = self.env["account.analytic.line"].browse(line_ids) + lines.filtered(lambda x: not x.is_approved).write({"is_approved": True}) + sale_orders = lines.mapped("order_id") + ctx = { + "active_model": "sale.order", + "active_ids": sale_orders.ids, + "open_invoices": True, + "timesheet_ids": line_ids, + } + payment = ( + self.env["sale.advance.payment.inv"] + .with_context(ctx) + .create( + { + "advance_payment_method": "delivered", + } + ) + ) + + return payment.create_invoices() diff --git a/sale_timesheet_approval/wizard/hr_timesheet_invoice_create_view.xml b/sale_timesheet_approval/wizard/hr_timesheet_invoice_create_view.xml new file mode 100644 index 0000000..808cfad --- /dev/null +++ b/sale_timesheet_approval/wizard/hr_timesheet_invoice_create_view.xml @@ -0,0 +1,33 @@ + + + + + hr.timesheet.invoice.create + +
+ + + + + + +
+
+
+
+
+ + + Approve + hr.timesheet.invoice.create + form + new + + list + + + +
diff --git a/sale_timesheet_approval/wizard/sale_make_invoice_advance.py b/sale_timesheet_approval/wizard/sale_make_invoice_advance.py new file mode 100644 index 0000000..7296935 --- /dev/null +++ b/sale_timesheet_approval/wizard/sale_make_invoice_advance.py @@ -0,0 +1,29 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class SaleAdvancePaymentInv(models.TransientModel): + _inherit = "sale.advance.payment.inv" + + def _create_invoices(self, sale_orders): + """Override method from sale/wizard/sale_make_invoice_advance.py + + When the user wants to invoice only the selected timesheets to the SO + then we need to recompute the qty_to_invoice for each product_id in + sale.order.line,before creating the invoice. + """ + if ( + self.advance_payment_method == "delivered" + and self.invoicing_timesheet_enabled + ): + if self._context.get("timesheet_ids"): + sale_orders.order_line.with_context( + self._context + )._recompute_qty_to_invoice_based_on_selected_timesheets() + + return sale_orders.with_context( + timesheet_ids=self._context["timesheet_ids"] + )._create_invoices(final=self.deduct_down_payments) + + return super()._create_invoices(sale_orders) diff --git a/sale_timesheet_approval_non_billable/README.rst b/sale_timesheet_approval_non_billable/README.rst new file mode 100644 index 0000000..cf31868 --- /dev/null +++ b/sale_timesheet_approval_non_billable/README.rst @@ -0,0 +1,11 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +=============================== +Timesheet Approval Non Billable +=============================== + +This module marks timesheet lines as Non Billable if the user just approves the timesheet lines(clicks on APPROVE button and not on APPROVE AND CREATE INVOICES button). + +* Anjeel Haria diff --git a/sale_timesheet_approval_non_billable/__init__.py b/sale_timesheet_approval_non_billable/__init__.py new file mode 100644 index 0000000..4b25b97 --- /dev/null +++ b/sale_timesheet_approval_non_billable/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import wizard diff --git a/sale_timesheet_approval_non_billable/__manifest__.py b/sale_timesheet_approval_non_billable/__manifest__.py new file mode 100644 index 0000000..cb1f71e --- /dev/null +++ b/sale_timesheet_approval_non_billable/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Timesheet Approval Non Billable", + "version": "16.0.1.0.0", + "summary": "Timesheet Approval management for lines marked as approved without creating invoices)", + "author": "Onestein", + "license": "AGPL-3", + "website": "https://www.onestein.nl", + "category": "Human Resources", + "depends": ["sale_timesheet_approval", "sale_timesheet_line_non_billable"], + "data": [], + "installable": True, + "auto_install": True, +} diff --git a/sale_timesheet_approval_non_billable/wizard/__init__.py b/sale_timesheet_approval_non_billable/wizard/__init__.py new file mode 100644 index 0000000..e42bb93 --- /dev/null +++ b/sale_timesheet_approval_non_billable/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import hr_timesheet_invoice_create diff --git a/sale_timesheet_approval_non_billable/wizard/hr_timesheet_invoice_create.py b/sale_timesheet_approval_non_billable/wizard/hr_timesheet_invoice_create.py new file mode 100644 index 0000000..185fe4a --- /dev/null +++ b/sale_timesheet_approval_non_billable/wizard/hr_timesheet_invoice_create.py @@ -0,0 +1,19 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class HrTimesheetInvoiceCreate(models.TransientModel): + _inherit = "hr.timesheet.invoice.create" + + def do_approve(self): + res = super().do_approve() + line_ids = self.env.context.get("active_ids") + lines = ( + self.env["account.analytic.line"] + .browse(line_ids) + .filtered(lambda x: not x.is_non_billable) + ) + lines.write({"is_non_billable": True}) + return res diff --git a/sale_timesheet_approval_sheet/README.rst b/sale_timesheet_approval_sheet/README.rst new file mode 100644 index 0000000..70f02b6 --- /dev/null +++ b/sale_timesheet_approval_sheet/README.rst @@ -0,0 +1,17 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +======================== +Timesheet Sheet Approval +======================== + +This module is a bridge between modules: sale_timesheet_approval and hr_timesheet_sheet. + +Credits +======= + +Contributors +------------ + +* Andrea Stirpe diff --git a/sale_timesheet_approval_sheet/__init__.py b/sale_timesheet_approval_sheet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sale_timesheet_approval_sheet/__manifest__.py b/sale_timesheet_approval_sheet/__manifest__.py new file mode 100644 index 0000000..8dc09ff --- /dev/null +++ b/sale_timesheet_approval_sheet/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2024 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Timesheet Sheet Approval", + "summary": "Timesheet Sheet Approval management", + "author": "Onestein", + "license": "AGPL-3", + "website": "https://www.onestein.nl", + "category": "Human Resources", + "version": "16.0.1.0.0", + "depends": [ + "sale_timesheet_approval", + "hr_timesheet_sheet", + ], + "data": [ + "views/hr_timesheet_views.xml", + ], + "auto_install": True, + "installable": True, +} diff --git a/sale_timesheet_approval_sheet/views/hr_timesheet_views.xml b/sale_timesheet_approval_sheet/views/hr_timesheet_views.xml new file mode 100644 index 0000000..40509bf --- /dev/null +++ b/sale_timesheet_approval_sheet/views/hr_timesheet_views.xml @@ -0,0 +1,20 @@ + + + + + account.analytic.line.tree + account.analytic.line + + + + + + + + + + + [('project_id', '!=', False),('is_approved', '=', False),('sheet_state', 'in', ['done']),('timesheet_invoice_id','=',False)] + + +