Skip to content

[ADD] pms_satisfaction_survey: send Odoo surveys to PMS guests after checkout#70

Open
DarioLodeiros wants to merge 2 commits into
16.0from
16.0-add-pms_satisfaction_survey
Open

[ADD] pms_satisfaction_survey: send Odoo surveys to PMS guests after checkout#70
DarioLodeiros wants to merge 2 commits into
16.0from
16.0-add-pms_satisfaction_survey

Conversation

@DarioLodeiros
Copy link
Copy Markdown
Member

Summary

New module pms_satisfaction_survey that wires native Odoo Surveys into the PMS so hotels can collect guest feedback automatically after every stay.

  • Opt-in per pms.property with configurable timing (on checkout / N hours after) and choice of survey.
  • Ships a default 5-question "Stay Satisfaction Survey" (EN + ES translation) with noupdate=1.
  • On the last reservation checkout of a folio, creates a tokenized survey.user_input bound to the folio and queues the standard invitation email via mail.mail.scheduled_date (the existing mail.ir_cron_mail_scheduler_action cron dispatches it).
  • Strict idempotency: one survey per folio; re-checkouts do not duplicate.
  • Adds folio_id and a stored related pms_property_id to survey.user_input so responses can be filtered by hotel/folio from Surveys → Participations.
  • Smart button on the folio form to jump to the linked response.

Verified locally

Installed on devel and ran an end-to-end smoke test in odoo shell:

  • Property A configured after_checkout +2h, partner en_US: survey.user_input created with folio_id/pms_property_id, mail.mail queued with scheduled_date = now + 2h, subject rendered in English.
  • Property B configured on_checkout, partner es_ES: scheduled_date = now, subject rendered in Spanish (partner.lang propagates via with_context(lang=...)).
  • Idempotency: re-firing the hook does not create a second user_input.
  • Property with the flag OFF: no user_input created.

Notes

  • requirements.txt was regenerated by the setuptools-odoo-get-requirements pre-commit hook, which collapsed the git+https://…/roomdoo-smartlocks… entries to bare package names. Plan is to clean this up via rebase after merging this PR to 16.0.
  • Module also needs to be enabled in aldahotels-odoo-16/odoo/custom/src/addons.yaml (handled in a separate change in that repo).

Test plan

  • Install pms_satisfaction_survey on a fresh pms DB.
  • Enable the survey on a property, choose timing, and verify defaults.
  • Check out all reservations of a folio and confirm survey.user_input + scheduled mail.mail appear.
  • Re-checkout to confirm idempotency.
  • Open the survey URL in the email and submit; confirm the response is visible from the folio smart button and from the Surveys list filtered by property/folio.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 20, 2026

Diff Coverage

Diff: origin/16.0...HEAD, staged and unstaged changes

  • pms_satisfaction_survey/models/pms_folio.py (42.4%): Missing lines 27-28,33-37,50,54-56,59-64,68-69,73-78,89,94,98-100,106-107,112-113
  • pms_satisfaction_survey/models/pms_property.py (80.0%): Missing lines 55-56,63,67
  • pms_satisfaction_survey/models/pms_reservation.py (100%)
  • pms_satisfaction_survey/models/survey_user_input.py (100%)

Summary

  • Total: 93 lines
  • Missing: 38 lines
  • Coverage: 59%

pms_satisfaction_survey/models/pms_folio.py

Lines 23-41

  23     )
  24 
  25     @api.depends("satisfaction_survey_user_input_id")
  26     def _compute_satisfaction_survey_user_input_count(self):
! 27         for folio in self:
! 28             folio.satisfaction_survey_user_input_count = (
  29                 1 if folio.satisfaction_survey_user_input_id else 0
  30             )
  31 
  32     def action_view_satisfaction_survey(self):
! 33         self.ensure_one()
! 34         user_input = self.satisfaction_survey_user_input_id
! 35         if not user_input:
! 36             return False
! 37         return {
  38             "type": "ir.actions.act_window",
  39             "name": _("Satisfaction Survey"),
  40             "res_model": "survey.user_input",
  41             "res_id": user_input.id,

Lines 46-82

  46     def _satisfaction_survey_should_schedule(self):
  47         """Return True if the folio is eligible to schedule a satisfaction survey."""
  48         self.ensure_one()
  49         if self.satisfaction_survey_user_input_id:
! 50             return False
  51         prop = self.pms_property_id
  52         if not prop or not prop.satisfaction_survey_enabled:
  53             return False
! 54         if not prop.satisfaction_survey_id:
! 55             return False
! 56         active_reservations = self.reservation_ids.filtered(
  57             lambda r: r.state != "cancel"
  58         )
! 59         if not active_reservations:
! 60             return False
! 61         if any(r.state != "done" for r in active_reservations):
! 62             return False
! 63         if not self.email:
! 64             _logger.info(
  65                 "Folio %s eligible for satisfaction survey but has no email; skipping.",
  66                 self.display_name,
  67             )
! 68             return False
! 69         return True
  70 
  71     def _satisfaction_survey_scheduled_date(self):
  72         """Return scheduled_date for the mail.mail per property settings."""
! 73         self.ensure_one()
! 74         prop = self.pms_property_id
! 75         now = fields.Datetime.now()
! 76         if prop.satisfaction_survey_send_moment == "after_checkout":
! 77             return now + timedelta(hours=prop.satisfaction_survey_send_delay_hours)
! 78         return now
  79 
  80     def _try_schedule_satisfaction_survey(self):
  81         """Create a survey.user_input for the folio and queue the invitation email.

Lines 85-104

   85         invite_template = self.env.ref(
   86             SURVEY_INVITE_TEMPLATE_XMLID, raise_if_not_found=False
   87         )
   88         if not invite_template:
!  89             _logger.warning(
   90                 "Survey invite mail template '%s' not found; cannot send "
   91                 "satisfaction surveys.",
   92                 SURVEY_INVITE_TEMPLATE_XMLID,
   93             )
!  94             return
   95         for folio in self:
   96             if not folio._satisfaction_survey_should_schedule():
   97                 continue
!  98             survey = folio.pms_property_id.satisfaction_survey_id
!  99             scheduled_date = folio._satisfaction_survey_scheduled_date()
! 100             user_input = survey.sudo()._create_answer(
  101                 partner=folio.partner_id or False,
  102                 email=folio.email,
  103                 check_attempts=False,
  104                 **{"folio_id": folio.id},

Lines 102-117

  102                 email=folio.email,
  103                 check_attempts=False,
  104                 **{"folio_id": folio.id},
  105             )
! 106             lang = folio.lang or self.env.lang
! 107             invite_template.with_context(lang=lang).send_mail(
  108                 user_input.id,
  109                 email_values={"scheduled_date": scheduled_date},
  110                 force_send=False,
  111             )
! 112             folio.satisfaction_survey_user_input_id = user_input
! 113             _logger.info(
  114                 "Satisfaction survey scheduled for folio %s (user_input %s, "
  115                 "scheduled_date %s).",
  116                 folio.display_name,
  117                 user_input.id,

pms_satisfaction_survey/models/pms_property.py

Lines 51-60

  51     def _check_satisfaction_survey_settings(self):
  52         for prop in self:
  53             if not prop.satisfaction_survey_enabled:
  54                 continue
! 55             if not prop.satisfaction_survey_id:
! 56                 raise ValidationError(
  57                     _(
  58                         "Property %s has satisfaction surveys enabled but no "
  59                         "survey is selected."
  60                     )

Lines 59-71

  59                         "survey is selected."
  60                     )
  61                     % prop.display_name
  62                 )
! 63             if (
  64                 prop.satisfaction_survey_send_moment == "after_checkout"
  65                 and prop.satisfaction_survey_send_delay_hours <= 0
  66             ):
! 67                 raise ValidationError(
  68                     _(
  69                         "Property %s: delay must be greater than zero when "
  70                         "sending the survey after checkout."
  71                     )

…checkout

Brings native Odoo Surveys into the PMS:

- Per-property opt-in with configurable timing (on checkout, or N hours
  after checkout) and pick of the survey to use.
- Ships a default 'Stay Satisfaction Survey' (5 questions, EN + ES) with
  noupdate=1 so customers can edit it freely.
- On the last reservation checkout of a folio, creates a tokenized
  survey.user_input bound to the folio and queues the standard invitation
  email via mail.mail.scheduled_date (dispatched by the existing
  mail.ir_cron_mail_scheduler_action cron).
- Strict idempotency: one survey per folio; re-checkout does not duplicate.
- Adds folio_id and a stored related pms_property_id to survey.user_input
  for fast filtering of responses by folio/property; exposes them in the
  tree/form/search views.
- Smart button on the folio form to jump to the linked response.
@DarioLodeiros DarioLodeiros force-pushed the 16.0-add-pms_satisfaction_survey branch from 265d0f5 to 514fcb8 Compare May 20, 2026 09:11
…of partner_id

Folios coming from OTA imports (Booking, Expedia, …) often have no
folio.partner_id but do carry the customer email in pms.folio.email (a
canonical computed-and-stored field on the folio) and folio.lang. The
previous implementation aborted in that case, leaving real-world OTA
checkouts without a survey invitation.

Switch to folio.email as the source of truth and treat partner_id as
optional; lang falls back to env.lang when absent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant