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)]}"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ of
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+