-
Notifications
You must be signed in to change notification settings - Fork 10
Fix/362 birthdate not future #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 19.0
Are you sure you want to change the base?
Changes from all commits
9325406
a4d572c
cd93398
4c2c750
af2f901
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,6 +104,22 @@ def _birthdate_onchange(self): | |
| } | ||
| } | ||
|
|
||
| @api.constrains("birthdate") | ||
| def _check_birthdate_not_future(self): | ||
| """Server-side backstop for future dates of birth. | ||
|
|
||
| ``_birthdate_onchange`` only runs in the form UI, so ORM | ||
| ``create`` / ``write``, CSV/Excel import, and API writes | ||
| (XML-RPC, API v2, DCI) bypass it and a future birthdate persists. | ||
| ``birthdate`` is a stored, writeable field, so this constraint | ||
| fires on every write path and keeps the non-stored ``age`` | ||
| compute from ever rendering a negative string. | ||
| """ | ||
| today = fields.Date.today() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Concrete scenario: a registrar in Pacific/Auckland (UTC+13 during DST) at 10:00 local on 2 January is at 21:00 UTC on 1 January. Recording a newborn born that morning, (The pre-existing |
||
| for record in self: | ||
| if record.birthdate and record.birthdate > today: | ||
| raise ValidationError(_("Date of birth cannot be in the future.")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message names neither the record nor the offending value, and Concrete scenario: a 5,000-row CSV import of registrants, one of which has a typo Separately, this new translatable string is not in |
||
|
|
||
| def _recompute_parent_groups(self, records): | ||
| field = self.env["res.partner"]._fields["force_recompute_canary"] | ||
| # Get the 'head' vocabulary code - this is a unique membership type | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| ### 19.0.2.1.5 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Version regression: the module is already at Also, Two follow-ups: bump the manifest and use the next version above the current head (e.g. |
||
|
|
||
| - fix(registry): reject future dates of birth on every write path. `_birthdate_onchange` only guards the form UI, so ORM `create`/`write`, CSV/Excel import, and API writes (XML-RPC, API v2, DCI) could persist a future `birthdate` — which the non-stored `age` compute then rendered as a negative string. A stored-field `@api.constrains("birthdate")` (`_check_birthdate_not_future`) now enforces this server-side; the onchange is kept as the friendlier silent-reset UX in the form (#362) | ||
|
|
||
| ### 19.0.2.2.2 | ||
|
|
||
| - fix(registry): let an ID type be used again after its ID was removed. Removing an ID through a change request keeps the row and marks it Invalid, and the old uniqueness rule counted those dead rows — so the registrant was left with an Invalid ID and no way to add a valid one of the same type. Uniqueness now applies to live IDs only, and is refused before the write so the message names the ID type rather than surfacing a database error (#1136) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -209,3 +209,50 @@ def test_duplicate_name_rejected(self): | |||||||||
| def test_empty_name_rejected(self): | ||||||||||
| with self.assertRaises(ValidationError): | ||||||||||
| self.IDType.create({"name": False}) | ||||||||||
|
|
||||||||||
| @tagged("post_install", "-at_install") | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only one blank line before the class decorator; PEP 8 /
Suggested change
|
||||||||||
| class TestBirthdateNotFutureConstraint(RegistryCommon): | ||||||||||
|
|
||||||||||
| def test_future_birthdate_rejected_on_write(self): | ||||||||||
| """A future birthdate set via write() raises ValidationError.""" | ||||||||||
| future = date.today() + timedelta(days=1) | ||||||||||
| with self.assertRaises(ValidationError): | ||||||||||
| self.individual_a.write({"birthdate": future}) | ||||||||||
|
|
||||||||||
| def test_future_birthdate_rejected_on_create(self): | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test passes on the base branch without the new constraint, so it does not actually cover the
|
||||||||||
| """A future birthdate passed to create() raises ValidationError.""" | ||||||||||
| future = date.today() + timedelta(days=1) | ||||||||||
| with self.assertRaises(ValidationError): | ||||||||||
| self.Partner.create( | ||||||||||
| { | ||||||||||
| "name": "Time Traveller", | ||||||||||
| "is_registrant": True, | ||||||||||
| "is_group": False, | ||||||||||
| "birthdate": future, | ||||||||||
| } | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| def test_future_birthdate_rejected_on_import(self): | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same vacuity as the Assert the message text instead, e.g. |
||||||||||
| future = date.today() + timedelta(days=1) | ||||||||||
| result = self.Partner.load( | ||||||||||
| ["name", "is_registrant", "is_group", "birthdate"], | ||||||||||
| [["Imported Person", "1", "0", str(future)]], | ||||||||||
| ) | ||||||||||
| self.assertTrue(result["messages"], "expected a constraint message from load()") | ||||||||||
| self.assertFalse(result["ids"], "the future-birthdate row must not be created") | ||||||||||
|
|
||||||||||
| def test_today_is_allowed(self): | ||||||||||
| """birthdate == today is the boundary that must pass.""" | ||||||||||
| self.individual_a.write({"birthdate": date.today()}) | ||||||||||
| self.assertEqual(self.individual_a.birthdate, date.today()) | ||||||||||
|
|
||||||||||
| def test_past_birthdate_allowed(self): | ||||||||||
| """An ordinary past birthdate writes without error.""" | ||||||||||
| self.individual_a.write({"birthdate": date(1990, 1, 1)}) | ||||||||||
| self.assertEqual(self.individual_a.birthdate, date(1990, 1, 1)) | ||||||||||
|
|
||||||||||
| def test_approximate_future_birthdate_rejected(self): | ||||||||||
| """An approximate DOB (birthdate_not_exact) still can't be future.""" | ||||||||||
| future = date.today() + timedelta(days=1) | ||||||||||
| with self.assertRaises(ValidationError): | ||||||||||
| self.individual_a.write({"birthdate": future, "birthdate_not_exact": True}) | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constraint closes the partner-side hole, but
spp_change_request_v2stores a proposed birthdate on its own detail models with no matching guard, so the failure now lands at the worst moment.spp_change_request_v2/details/create_group.py:369(birthdate = fields.Date(...)),details/edit_individual.py:54andwizards/create_group_member_wizard.py:179all accept a future date;details/create_group.py:426-434_compute_ageeven clamps the result to0, so the UI shows nothing wrong. The value only reachesres.partnerat apply time (strategies/add_member.py:35,50andstrategies/create_group.py:321), which is called unguarded fromchange_request.py:1521(strategy.apply(sudo_self)).Concrete scenario: a CR with a future DOB is submitted, reviewed and approved; on the final approve the
res.partner.createraisesValidationError, the whole approval transaction rolls back, and the approver sees "Date of birth cannot be in the future." with no indication which CR field caused it. Before this PR the CR applied (badly, but successfully). Worth mirroring the check on the CR detail/wizard models so it is caught at data entry.