diff --git a/hr_timesheet_purchase_order/__manifest__.py b/hr_timesheet_purchase_order/__manifest__.py index c5e5f1b156..035b0da9d7 100644 --- a/hr_timesheet_purchase_order/__manifest__.py +++ b/hr_timesheet_purchase_order/__manifest__.py @@ -14,6 +14,7 @@ "demo": [], "data": [ "data/ir_actions_server.xml", + "data/hr_timesheet_cron.xml", "views/hr_employee_view.xml", "views/hr_timesheet_sheet_view.xml", "views/purchase_order_view.xml", diff --git a/hr_timesheet_purchase_order/data/hr_timesheet_cron.xml b/hr_timesheet_purchase_order/data/hr_timesheet_cron.xml new file mode 100644 index 0000000000..5f27a5862b --- /dev/null +++ b/hr_timesheet_purchase_order/data/hr_timesheet_cron.xml @@ -0,0 +1,11 @@ + + + + HR Timesheet : Auto generate Purchase Order + + code + model._cron_generate_auto_po() + days + -1 + + diff --git a/hr_timesheet_purchase_order/models/hr_employee_base.py b/hr_timesheet_purchase_order/models/hr_employee_base.py index 4b5359484d..06e71ca8de 100644 --- a/hr_timesheet_purchase_order/models/hr_employee_base.py +++ b/hr_timesheet_purchase_order/models/hr_employee_base.py @@ -1,4 +1,56 @@ -from odoo import fields, models +from calendar import monthrange +from datetime import timedelta + +from dateutil.relativedelta import relativedelta +from dateutil.rrule import ( + DAILY, + FR, + MO, + MONTHLY, + SA, + SU, + TH, + TU, + WE, + WEEKLY, + YEARLY, + rrule, +) + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +MONTHS = { + "january": 31, + "february": 28, + "march": 31, + "april": 30, + "may": 31, + "june": 30, + "july": 31, + "august": 31, + "september": 30, + "october": 31, + "november": 30, + "december": 31, +} + +DAYS = { + "mon": MO, + "tue": TU, + "wed": WE, + "thu": TH, + "fri": FR, + "sat": SA, + "sun": SU, +} + +WEEKS = { + "first": 1, + "second": 2, + "third": 3, + "last": 4, +} class HrEmployeeBase(models.AbstractModel): @@ -8,3 +60,405 @@ class HrEmployeeBase(models.AbstractModel): string="Generate POs from Timesheet", default=False ) billing_partner_id = fields.Many2one("res.partner") + + is_auto_po_generate = fields.Boolean( + string="Automatic PO generation", default=False + ) + + is_send_po = fields.Boolean( + string="Send RFQ by email after creation", default=False + ) + next_recurrence_date = fields.Date() + recurrence_left = fields.Integer(string="Number of tasks left to create") + repeat_interval = fields.Integer( + string="Repeat Every", default=1, compute="_compute_repeat", readonly=False + ) + repeat_unit = fields.Selection( + [ + ("day", "Days"), + ("week", "Weeks"), + ("month", "Months"), + ("year", "Years"), + ], + default="week", + compute="_compute_repeat", + readonly=False, + ) + repeat_type = fields.Selection( + [ + ("forever", "Forever"), + ("until", "End Date"), + ("after", "Number of Repetitions"), + ], + default="forever", + string="Until", + compute="_compute_repeat", + readonly=False, + ) + repeat_until = fields.Date( + string="End Date", compute="_compute_repeat", readonly=False + ) + repeat_number = fields.Integer( + string="Repetitions", default=1, compute="_compute_repeat", readonly=False + ) + + repeat_on_month = fields.Selection( + [ + ("date", "Date of the Month"), + ("day", "Day of the Month"), + ], + default="date", + compute="_compute_repeat", + readonly=False, + ) + + repeat_on_year = fields.Selection( + [ + ("date", "Date of the Year"), + ("day", "Day of the Year"), + ], + default="date", + compute="_compute_repeat", + readonly=False, + ) + + mon = fields.Boolean(string="Mon", compute="_compute_repeat", readonly=False) + tue = fields.Boolean(string="Tue", compute="_compute_repeat", readonly=False) + wed = fields.Boolean(string="Wed", compute="_compute_repeat", readonly=False) + thu = fields.Boolean(string="Thu", compute="_compute_repeat", readonly=False) + fri = fields.Boolean(string="Fri", compute="_compute_repeat", readonly=False) + sat = fields.Boolean(string="Sat", compute="_compute_repeat", readonly=False) + sun = fields.Boolean(string="Sun", compute="_compute_repeat", readonly=False) + + repeat_day = fields.Selection( + [(str(i), str(i)) for i in range(1, 32)], + compute="_compute_repeat", + readonly=False, + ) + repeat_week = fields.Selection( + [ + ("first", "First"), + ("second", "Second"), + ("third", "Third"), + ("last", "Last"), + ], + default="first", + compute="_compute_repeat", + readonly=False, + ) + repeat_weekday = fields.Selection( + [ + ("mon", "Monday"), + ("tue", "Tuesday"), + ("wed", "Wednesday"), + ("thu", "Thursday"), + ("fri", "Friday"), + ("sat", "Saturday"), + ("sun", "Sunday"), + ], + string="Day Of The Week", + compute="_compute_repeat", + readonly=False, + ) + repeat_month = fields.Selection( + [ + ("january", "January"), + ("february", "February"), + ("march", "March"), + ("april", "April"), + ("may", "May"), + ("june", "June"), + ("july", "July"), + ("august", "August"), + ("september", "September"), + ("october", "October"), + ("november", "November"), + ("december", "December"), + ], + compute="_compute_repeat", + readonly=False, + ) + + repeat_show_dow = fields.Boolean(compute="_compute_repeat_visibility") + repeat_show_day = fields.Boolean(compute="_compute_repeat_visibility") + repeat_show_week = fields.Boolean(compute="_compute_repeat_visibility") + repeat_show_month = fields.Boolean(compute="_compute_repeat_visibility") + + @api.depends( + "is_auto_po_generate", "repeat_unit", "repeat_on_month", "repeat_on_year" + ) + def _compute_repeat_visibility(self): + for item in self: + item.repeat_show_day = ( + item.is_auto_po_generate + and (item.repeat_unit == "month" and item.repeat_on_month == "date") + or (item.repeat_unit == "year" and item.repeat_on_year == "date") + ) + item.repeat_show_week = ( + item.is_auto_po_generate + and (item.repeat_unit == "month" and item.repeat_on_month == "day") + or (item.repeat_unit == "year" and item.repeat_on_year == "day") + ) + item.repeat_show_dow = ( + item.is_auto_po_generate and item.repeat_unit == "week" + ) + item.repeat_show_month = ( + item.is_auto_po_generate and item.repeat_unit == "year" + ) + + @api.depends("is_auto_po_generate") + def _compute_repeat(self): + rec_fields = self._get_recurrence_fields() + defaults = self.default_get(rec_fields) + for item in self: + for f in rec_fields: + if item.is_auto_po_generate: + item[f] = defaults.get(f) + else: + item[f] = False + + @api.model + def _get_recurrence_fields(self): + return [ + "repeat_interval", + "repeat_unit", + "repeat_type", + "repeat_until", + "repeat_number", + "repeat_on_month", + "repeat_on_year", + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun", + "repeat_day", + "repeat_week", + "repeat_month", + "repeat_weekday", + ] + + @api.model + def default_get(self, default_fields): + vals = super().default_get(default_fields) + days = list(DAYS.keys()) + week_start = fields.Datetime.today().weekday() + if all(d in default_fields for d in days): + vals[days[week_start]] = True + if "repeat_day" in default_fields: + vals["repeat_day"] = str(fields.Datetime.today().day) + if "repeat_month" in default_fields: + vals["repeat_month"] = self._fields.get("repeat_month").selection[ + fields.Datetime.today().month - 1 + ][0] + if "repeat_until" in default_fields: + vals["repeat_until"] = fields.Date.today() + timedelta(days=7) + if "repeat_weekday" in default_fields: + vals["repeat_weekday"] = self._fields.get("repeat_weekday").selection[ + week_start + ][0] + + return vals + + @api.constrains("repeat_unit", "mon", "tue", "wed", "thu", "fri", "sat", "sun") + def _check_recurrence_days(self): + for timesheet in self.filtered(lambda p: p.repeat_unit == "week"): + if not any( + [ + timesheet.mon, + timesheet.tue, + timesheet.wed, + timesheet.thu, + timesheet.fri, + timesheet.sat, + timesheet.sun, + ] + ): + raise ValidationError(_("You should select a least one day")) + + def _check_next_recurrence_date(self): + for record in self.filtered( + lambda t: t.is_auto_po_generate and not t.next_recurrence_date + ): + record._set_next_recurrence_date() + + @api.constrains("repeat_interval") + def _check_repeat_interval(self): + if self.filtered(lambda t: t.repeat_interval <= 0 and t.is_auto_po_generate): + raise ValidationError(_("The interval should be greater than 0")) + + @api.constrains("repeat_number", "repeat_type") + def _check_repeat_number(self): + if self.filtered(lambda t: t.repeat_type == "after" and t.repeat_number <= 0): + raise ValidationError(_("Should repeat at least once")) + + @api.constrains("repeat_type", "repeat_until") + def _check_repeat_until_date(self): + today = fields.Date.today() + if self.filtered(lambda t: t.repeat_type == "until" and t.repeat_until < today): + raise ValidationError(_("The end date should be in the future")) + + @api.constrains( + "repeat_unit", "repeat_on_month", "repeat_day", "repeat_type", "repeat_until" + ) + def _check_repeat_until_month(self): + if self.filtered( + lambda r: r.repeat_type == "until" + and r.repeat_unit == "month" + and r.repeat_until + and r.repeat_on_month == "date" + and int(r.repeat_day) > r.repeat_until.day + and monthrange(r.repeat_until.year, r.repeat_until.month)[1] + != r.repeat_until.day + ): + raise ValidationError( + _( + "The end date should be after the day of " + "the month or the last day of the month" + ) + ) + + def _set_next_recurrence_date(self): + today = fields.Date.today() + tomorrow = today + relativedelta(days=1) + for recurrence in self.filtered( + lambda r: r.repeat_type == "after" + and r.recurrence_left >= 0 + or r.repeat_type == "until" + and r.repeat_until >= today + or r.repeat_type == "forever" + ): + if recurrence.repeat_type == "after" and recurrence.recurrence_left == 0: + recurrence.next_recurrence_date = False + else: + next_date = self._get_next_recurring_dates( + tomorrow, + recurrence.repeat_interval, + recurrence.repeat_unit, + recurrence.repeat_type, + recurrence.repeat_until, + recurrence.repeat_on_month, + recurrence.repeat_on_year, + recurrence._get_weekdays(), + recurrence.repeat_day, + recurrence.repeat_week, + recurrence.repeat_month, + count=1, + ) + recurrence.next_recurrence_date = next_date[0] if next_date else False + + def _get_weekdays(self, n=1): + self.ensure_one() + if self.repeat_unit == "week": + return [fn(n) for day, fn in DAYS.items() if self[day]] + return [DAYS.get(self.repeat_weekday)(n)] + + @api.model + def _get_next_recurring_dates( + self, + date_start, + repeat_interval, + repeat_unit, + repeat_type, + repeat_until, + repeat_on_month, + repeat_on_year, + weekdays, + repeat_day, + repeat_week, + repeat_month, + **kwargs + ): + count = kwargs.get("count", 1) + rrule_kwargs = {"interval": repeat_interval or 1, "dtstart": date_start} + repeat_day = int(repeat_day) + start = False + dates = [] + if repeat_type == "until": + rrule_kwargs["until"] = ( + repeat_until if repeat_until else fields.Date.today() + ) + else: + rrule_kwargs["count"] = count + + if ( + repeat_unit == "week" + or (repeat_unit == "month" and repeat_on_month == "day") + or (repeat_unit == "year" and repeat_on_year == "day") + ): + rrule_kwargs["byweekday"] = weekdays + + if repeat_unit == "day": + rrule_kwargs["freq"] = DAILY + elif repeat_unit == "month": + rrule_kwargs["freq"] = MONTHLY + if repeat_on_month == "date": + start = date_start - relativedelta(days=1) + start = start.replace( + day=min(repeat_day, monthrange(start.year, start.month)[1]) + ) + if start < date_start: + # Ensure the next recurrence is in the future + start += relativedelta(months=repeat_interval) + start = start.replace( + day=min(repeat_day, monthrange(start.year, start.month)[1]) + ) + can_generate_date = ( + (lambda: start <= repeat_until) + if repeat_type == "until" + else (lambda: len(dates) < count) + ) + while can_generate_date(): + dates.append(start) + start += relativedelta(months=repeat_interval) + start = start.replace( + day=min(repeat_day, monthrange(start.year, start.month)[1]) + ) + return dates + elif repeat_unit == "year": + rrule_kwargs["freq"] = YEARLY + month = list(MONTHS.keys()).index(repeat_month) + 1 + rrule_kwargs["bymonth"] = month + if repeat_on_year == "date": + rrule_kwargs["bymonthday"] = min(repeat_day, MONTHS.get(repeat_month)) + rrule_kwargs["bymonth"] = month + else: + rrule_kwargs["freq"] = WEEKLY + + rules = rrule(**rrule_kwargs) + return list(rules) if rules else [] + + def _create_purchase_order(self): + for item in self: + timesheet = item.timesheet_sheet_ids.filtered( + lambda t: not t.purchase_order_id and t.state == "done" + ) + if not timesheet: + continue + timesheet = timesheet[0] + timesheet.action_create_purchase_order() + if item.is_send_po: + email_act = timesheet.purchase_order_id.action_rfq_send() + email_ctx = email_act.get("context", {}) + timesheet.purchase_order_id.with_context( + **email_ctx + ).message_post_with_template(email_ctx.get("default_template_id")) + + @api.model + def _cron_generate_auto_po(self): + today = fields.Date.today() + recurring_today = self.search( + [("next_recurrence_date", "<=", today), ("is_auto_po_generate", "=", True)] + ) + recurring_today._create_purchase_order() + for recurrence in recurring_today.filtered(lambda r: r.repeat_type == "after"): + recurrence.recurrence_left -= 1 + recurring_today._set_next_recurrence_date() + + def write(self, vals): + res = super().write(vals) + if "next_recurrence_date" not in vals: + self._check_next_recurrence_date() + return res diff --git a/hr_timesheet_purchase_order/tests/__init__.py b/hr_timesheet_purchase_order/tests/__init__.py index 397cdf0309..d8930920ad 100644 --- a/hr_timesheet_purchase_order/tests/__init__.py +++ b/hr_timesheet_purchase_order/tests/__init__.py @@ -1 +1,2 @@ from . import test_create_timesheet_purchase_order +from . import test_create_timesheet_po_recurrence diff --git a/hr_timesheet_purchase_order/tests/test_create_timesheet_po_recurrence.py b/hr_timesheet_purchase_order/tests/test_create_timesheet_po_recurrence.py new file mode 100644 index 0000000000..cba981f3aa --- /dev/null +++ b/hr_timesheet_purchase_order/tests/test_create_timesheet_po_recurrence.py @@ -0,0 +1,374 @@ +from datetime import date, datetime + +from dateutil.rrule import FR, MO, SA, TH +from freezegun import freeze_time + +from odoo.tests.common import Form, SavepointCase + + +class TestTimesheetPOrecurrence(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.product = cls.env["product.product"].create( + { + "name": "Product recurrence", + "default_code": "test", + } + ) + officer_group = cls.env.ref("hr.group_hr_user") + multi_company_group = cls.env.ref("base.group_multi_company") + sheet_user_group = cls.env.ref("hr_timesheet.group_hr_timesheet_user") + project_user_group = cls.env.ref("project.group_project_user") + cls.sheet_model = cls.env["hr_timesheet.sheet"].with_context( + tracking_disable=True + ) + cls.sheet_line_model = cls.env["hr_timesheet.sheet.line"] + cls.project_model = cls.env["project.project"] + cls.task_model = cls.env["project.task"] + cls.aal_model = cls.env["account.analytic.line"] + cls.aaa_model = cls.env["account.analytic.account"] + cls.employee_model = cls.env["hr.employee"] + cls.department_model = cls.env["hr.department"] + config_obj = cls.env["res.config.settings"] + config = config_obj.create({"timesheet_product_id": cls.product.id}) + config.execute() + + cls.user = ( + cls.env["res.users"] + .with_context(no_reset_password=True) + .create( + { + "name": "Test User recurrence", + "login": "test_user_recurrence", + "email": "test_recurrence@oca.com", + "groups_id": [ + ( + 6, + 0, + [ + officer_group.id, + sheet_user_group.id, + project_user_group.id, + multi_company_group.id, + ], + ) + ], + } + ) + ) + + cls.project = cls.project_model.create( + { + "name": "Project", + "allow_timesheets": True, + "user_id": cls.user.id, + } + ) + cls.task = cls.task_model.create( + { + "name": "Task 1", + "project_id": cls.project.id, + } + ) + + def create_prepare_data(self): + sheet_form = Form(self.sheet_model.with_user(self.user)) + with sheet_form.timesheet_ids.new() as timesheet: + timesheet.name = "test" + timesheet.project_id = self.project + + with sheet_form.timesheet_ids.edit(0) as timesheet: + timesheet.unit_amount = 1.0 + sheet_form.save() + + def test_create_purchase_order_recurrence_simple(self): + with freeze_time("2020-02-01"): + employee = self.employee_model.create( + { + "name": "Test Employee", + "user_id": self.user.id, + "billing_partner_id": self.user.partner_id.id, + "allow_generate_purchase_order": True, + "is_auto_po_generate": True, + "repeat_interval": 5, + "repeat_unit": "month", + "repeat_type": "after", + "repeat_number": 10, + "repeat_on_month": "date", + "repeat_day": "31", + } + ) + + self.assertTrue( + bool(employee.is_auto_po_generate), "should enable a recurrence" + ) + employee.write(dict(repeat_interval=2, repeat_number=11)) + self.assertEqual( + employee.repeat_interval, 2, "recurrence should be updated" + ) + self.assertEqual(employee.repeat_number, 11, "recurrence should be updated") + self.assertEqual(employee.recurrence_left, 11) + + employee._check_next_recurrence_date() + self.assertEqual(employee.next_recurrence_date, date(2020, 2, 29)) + + employee.is_auto_po_generate = False + self.assertFalse( + bool(employee.is_auto_po_generate), "the recurrence should be disabled" + ) + + def test_recurrence_cron_repeat_after(self): + self.create_prepare_data() + with freeze_time("2020-01-01"): + form = Form(self.employee_model) + form.name = "Test Employee recurrence" + form.user_id = self.user.id + form.billing_partner_id = self.user.partner_id.id + form.allow_generate_purchase_order = True + form.is_auto_po_generate = True + + form.recurring_task = True + form.repeat_interval = 1 + form.repeat_unit = "month" + form.repeat_type = "after" + form.repeat_number = 2 + form.repeat_on_month = "date" + form.repeat_day = "15" + employee = form.save() + + self.assertEqual(employee.next_recurrence_date, date(2020, 1, 15)) + self.assertEqual(len(employee.timesheet_sheet_ids), 1) + + self.employee_model._cron_generate_auto_po() + self.assertEqual( + len(employee.timesheet_sheet_ids), 1, "no extra task should be created" + ) + self.assertEqual(employee.recurrence_left, 2) + + def test_recurrence_next_dates_week(self): + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2020, 1, 1), + repeat_interval=1, + repeat_unit="week", + repeat_type=False, + repeat_until=False, + repeat_on_month=False, + repeat_on_year=False, + weekdays=False, + repeat_day=False, + repeat_week=False, + repeat_month=False, + count=5, + ) + + self.assertEqual(dates[0], datetime(2020, 1, 6, 0, 0)) + self.assertEqual(dates[1], datetime(2020, 1, 13, 0, 0)) + self.assertEqual(dates[2], datetime(2020, 1, 20, 0, 0)) + self.assertEqual(dates[3], datetime(2020, 1, 27, 0, 0)) + self.assertEqual(dates[4], datetime(2020, 2, 3, 0, 0)) + + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2020, 1, 1), + repeat_interval=3, + repeat_unit="week", + repeat_type="until", + repeat_until=date(2020, 2, 1), + repeat_on_month=False, + repeat_on_year=False, + weekdays=[MO, FR], + repeat_day=False, + repeat_week=False, + repeat_month=False, + count=100, + ) + + self.assertEqual(len(dates), 3) + self.assertEqual(dates[0], datetime(2020, 1, 3, 0, 0)) + self.assertEqual(dates[1], datetime(2020, 1, 20, 0, 0)) + self.assertEqual(dates[2], datetime(2020, 1, 24, 0, 0)) + + def test_recurrence_next_dates_month(self): + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2020, 1, 15), + repeat_interval=1, + repeat_unit="month", + repeat_type=False, # Forever + repeat_until=False, + repeat_on_month="date", + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=12, + ) + + # should take the last day of each month + self.assertEqual(dates[0], date(2020, 1, 31)) + self.assertEqual(dates[1], date(2020, 2, 29)) + self.assertEqual(dates[2], date(2020, 3, 31)) + self.assertEqual(dates[3], date(2020, 4, 30)) + self.assertEqual(dates[4], date(2020, 5, 31)) + self.assertEqual(dates[5], date(2020, 6, 30)) + self.assertEqual(dates[6], date(2020, 7, 31)) + self.assertEqual(dates[7], date(2020, 8, 31)) + self.assertEqual(dates[8], date(2020, 9, 30)) + self.assertEqual(dates[9], date(2020, 10, 31)) + self.assertEqual(dates[10], date(2020, 11, 30)) + self.assertEqual(dates[11], date(2020, 12, 31)) + + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2020, 2, 20), + repeat_interval=3, + repeat_unit="month", + repeat_type=False, # Forever + repeat_until=False, + repeat_on_month="date", + repeat_on_year=False, + weekdays=False, + repeat_day=29, + repeat_week=False, + repeat_month=False, + count=5, + ) + + self.assertEqual(dates[0], date(2020, 2, 29)) + self.assertEqual(dates[1], date(2020, 5, 29)) + self.assertEqual(dates[2], date(2020, 8, 29)) + self.assertEqual(dates[3], date(2020, 11, 29)) + self.assertEqual(dates[4], date(2021, 2, 28)) + + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2020, 1, 10), + repeat_interval=1, + repeat_unit="month", + repeat_type="until", + repeat_until=datetime(2020, 5, 31), + repeat_on_month="day", + repeat_on_year=False, + weekdays=[ + SA(4), + ], # 4th Saturday + repeat_day=29, + repeat_week=False, + repeat_month=False, + count=6, + ) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], datetime(2020, 1, 25)) + self.assertEqual(dates[1], datetime(2020, 2, 22)) + self.assertEqual(dates[2], datetime(2020, 3, 28)) + self.assertEqual(dates[3], datetime(2020, 4, 25)) + self.assertEqual(dates[4], datetime(2020, 5, 23)) + + dates = self.employee_model._get_next_recurring_dates( + date_start=datetime(2020, 1, 10), + repeat_interval=6, # twice a year + repeat_unit="month", + repeat_type="until", + repeat_until=datetime(2021, 1, 11), + repeat_on_month="date", + repeat_on_year=False, + weekdays=[TH(+1)], + repeat_day="3", # the 3rd of the month + repeat_week=False, + repeat_month=False, + count=1, + ) + + self.assertEqual(len(dates), 2) + self.assertEqual(dates[0], datetime(2020, 7, 3)) + self.assertEqual(dates[1], datetime(2021, 1, 3)) + + # Should generate a date at the last day of the current month + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2022, 2, 26), + repeat_interval=1, + repeat_unit="month", + repeat_type="until", + repeat_until=date(2022, 2, 28), + repeat_on_month="date", + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=5, + ) + + self.assertEqual(len(dates), 1) + self.assertEqual(dates[0], date(2022, 2, 28)) + + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2022, 11, 26), + repeat_interval=3, + repeat_unit="month", + repeat_type="until", + repeat_until=date(2024, 2, 29), + repeat_on_month="date", + repeat_on_year=False, + weekdays=False, + repeat_day=25, + repeat_week=False, + repeat_month=False, + count=5, + ) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], date(2023, 2, 25)) + self.assertEqual(dates[1], date(2023, 5, 25)) + self.assertEqual(dates[2], date(2023, 8, 25)) + self.assertEqual(dates[3], date(2023, 11, 25)) + self.assertEqual(dates[4], date(2024, 2, 25)) + + # Use the exact same parameters than the previous test + # but with a repeat_day that is not passed yet + # So we generate an additional date in the current month + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2022, 11, 26), + repeat_interval=3, + repeat_unit="month", + repeat_type="until", + repeat_until=date(2024, 2, 29), + repeat_on_month="date", + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=5, + ) + + self.assertEqual(len(dates), 6) + self.assertEqual(dates[0], date(2022, 11, 30)) + self.assertEqual(dates[1], date(2023, 2, 28)) + self.assertEqual(dates[2], date(2023, 5, 31)) + self.assertEqual(dates[3], date(2023, 8, 31)) + self.assertEqual(dates[4], date(2023, 11, 30)) + self.assertEqual(dates[5], date(2024, 2, 29)) + + def test_recurrence_next_dates_year(self): + dates = self.employee_model._get_next_recurring_dates( + date_start=date(2020, 12, 1), + repeat_interval=1, + repeat_unit="year", + repeat_type="until", + repeat_until=datetime(2026, 1, 1), + repeat_on_month=False, + repeat_on_year="date", + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month="november", + count=10, + ) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], datetime(2021, 11, 30)) + self.assertEqual(dates[1], datetime(2022, 11, 30)) + self.assertEqual(dates[2], datetime(2023, 11, 30)) + self.assertEqual(dates[3], datetime(2024, 11, 30)) + self.assertEqual(dates[4], datetime(2025, 11, 30)) diff --git a/hr_timesheet_purchase_order/views/hr_employee_view.xml b/hr_timesheet_purchase_order/views/hr_employee_view.xml index b7ad870210..6d3d5348ac 100644 --- a/hr_timesheet_purchase_order/views/hr_employee_view.xml +++ b/hr_timesheet_purchase_order/views/hr_employee_view.xml @@ -15,6 +15,123 @@ name="billing_partner_id" attrs="{'required': [('allow_generate_purchase_order', '=', True)], 'invisible': [('allow_generate_purchase_order', '=', False)]}" /> + + + + + + + + + + +