From cc80d4b1629be48cebacd308d5bba9599f0db36e Mon Sep 17 00:00:00 2001 From: Daniel Gergely Date: Mon, 22 Jun 2026 17:57:22 +0200 Subject: [PATCH 1/5] [T3241] FIX: stamp exit communication on send completion (_job_sent) --- .../models/partner_communication.py | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/partner_communication_compassion/models/partner_communication.py b/partner_communication_compassion/models/partner_communication.py index 7aaf43612..50f5fe52e 100644 --- a/partner_communication_compassion/models/partner_communication.py +++ b/partner_communication_compassion/models/partner_communication.py @@ -163,7 +163,6 @@ def send(self): if biennials: for child in biennials.get_objects(): child.sponsorship_ids[0].new_picture = False - self._stamp_exit_communications() return res def cancel(self): @@ -173,29 +172,42 @@ def cancel(self): if biennials: for child in biennials.get_objects(): child.sponsorship_ids[0].new_picture = False - self._stamp_exit_communications() + exit_confs = self._exit_communication_configs() + self.filtered( + lambda j: j.config_id in exit_confs + )._stamp_exit_communications() return res - def _stamp_exit_communications(self): - """Mark exit communications as handled: stamp exit_communication_sent - and run the deferred invoice cleanup. A cancelled exit communication - counts the same as a sent one, because SDS sometimes informs the sponsor - by phone and simply cancels the auto-generated exit communication. - """ - exit_confs = self.env.ref( + def _exit_communication_configs(self): + """Communication configs that notify the sponsor of a child's exit.""" + return self.env.ref( "partner_communication_compassion.lifecycle_child_planned_exit" ) + self.env.ref( "partner_communication_compassion.lifecycle_child_unplanned_exit" ) - exits = self.filtered( - lambda j: j.state in ("done", "cancel") and j.config_id in exit_confs - ) + + def _stamp_exit_communications(self): + """For the exit communications in self that are now sent or cancelled, + stamp exit_communication_sent on their contracts and run the deferred + invoice cleanup. A cancelled exit communication counts the same as a + sent one (SDS sometimes informs the sponsor by phone and cancels the + auto-generated exit communication). + """ + exits = self.filtered(lambda j: j.state in ("done", "cancel")) if exits: contracts = exits.get_objects() - # Capture the departures still pending cleanup BEFORE the stamp below - # flips exit_communication_pending to False; only those need the - # deferred invoice cleanup (avoids re-triggering it on re-sends or - # contracts already cleaned by the old flow). + # Run the deferred invoice cleanup only on contracts still pending + # (captured before the stamp clears it) so a second exit comm for the + # same contract doesn't clean twice. to_clean = contracts.filtered("exit_communication_pending") contracts.write({"exit_communication_sent": fields.Datetime.now()}) to_clean.cancel_contract_invoices() + + def _job_sent(self, send_mode): + res = super()._job_sent(send_mode) + # Set exit communications (only) to completed + exit_confs = self._exit_communication_configs() + self.filtered( + lambda j: j.config_id in exit_confs + )._stamp_exit_communications() + return res From b467cbf8c063e36b6094ef84ea8b52e9fca56a8a Mon Sep 17 00:00:00 2001 From: Daniel Gergely Date: Tue, 23 Jun 2026 07:13:43 +0200 Subject: [PATCH 2/5] FIX: GCA&Greptile fix --- .../models/partner_communication.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/partner_communication_compassion/models/partner_communication.py b/partner_communication_compassion/models/partner_communication.py index 50f5fe52e..5201bea0b 100644 --- a/partner_communication_compassion/models/partner_communication.py +++ b/partner_communication_compassion/models/partner_communication.py @@ -187,21 +187,17 @@ def _exit_communication_configs(self): ) def _stamp_exit_communications(self): - """For the exit communications in self that are now sent or cancelled, - stamp exit_communication_sent on their contracts and run the deferred - invoice cleanup. A cancelled exit communication counts the same as a - sent one (SDS sometimes informs the sponsor by phone and cancels the - auto-generated exit communication). + """Stamp the still-pending contracts of these (sent or cancelled) exit + communications and run their deferred invoice cleanup. """ - exits = self.filtered(lambda j: j.state in ("done", "cancel")) - if exits: - contracts = exits.get_objects() - # Run the deferred invoice cleanup only on contracts still pending - # (captured before the stamp clears it) so a second exit comm for the - # same contract doesn't clean twice. - to_clean = contracts.filtered("exit_communication_pending") - contracts.write({"exit_communication_sent": fields.Datetime.now()}) - to_clean.cancel_contract_invoices() + if not self: + return + # Only pending contracts: skips ones already handled (re-send, duplicate + # job, or the second leg of a "both" communication). + pending = self.get_objects().filtered("exit_communication_pending") + if pending: + pending.write({"exit_communication_sent": fields.Datetime.now()}) + pending.cancel_contract_invoices() def _job_sent(self, send_mode): res = super()._job_sent(send_mode) From 6f6b2e4aaab564bf5caeab1296f53fbaa2182dd8 Mon Sep 17 00:00:00 2001 From: Daniel Gergely Date: Tue, 23 Jun 2026 07:17:54 +0200 Subject: [PATCH 3/5] STYLE: pre-commit --- .../models/partner_communication.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/partner_communication_compassion/models/partner_communication.py b/partner_communication_compassion/models/partner_communication.py index 5201bea0b..e8cf168e5 100644 --- a/partner_communication_compassion/models/partner_communication.py +++ b/partner_communication_compassion/models/partner_communication.py @@ -173,9 +173,7 @@ def cancel(self): for child in biennials.get_objects(): child.sponsorship_ids[0].new_picture = False exit_confs = self._exit_communication_configs() - self.filtered( - lambda j: j.config_id in exit_confs - )._stamp_exit_communications() + self.filtered(lambda j: j.config_id in exit_confs)._stamp_exit_communications() return res def _exit_communication_configs(self): @@ -203,7 +201,5 @@ def _job_sent(self, send_mode): res = super()._job_sent(send_mode) # Set exit communications (only) to completed exit_confs = self._exit_communication_configs() - self.filtered( - lambda j: j.config_id in exit_confs - )._stamp_exit_communications() + self.filtered(lambda j: j.config_id in exit_confs)._stamp_exit_communications() return res From ad0cc772940a3f5b54fc2454750d149ecf26f8ac Mon Sep 17 00:00:00 2001 From: Daniel Gergely Date: Tue, 23 Jun 2026 08:26:00 +0200 Subject: [PATCH 4/5] REFACTOR: Finalize exit communication on send completion --- .../models/partner_communication.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/partner_communication_compassion/models/partner_communication.py b/partner_communication_compassion/models/partner_communication.py index e8cf168e5..39568e4cd 100644 --- a/partner_communication_compassion/models/partner_communication.py +++ b/partner_communication_compassion/models/partner_communication.py @@ -173,7 +173,9 @@ def cancel(self): for child in biennials.get_objects(): child.sponsorship_ids[0].new_picture = False exit_confs = self._exit_communication_configs() - self.filtered(lambda j: j.config_id in exit_confs)._stamp_exit_communications() + self.filtered( + lambda j: j.config_id in exit_confs + )._finalize_exit_communications() return res def _exit_communication_configs(self): @@ -184,22 +186,26 @@ def _exit_communication_configs(self): "partner_communication_compassion.lifecycle_child_unplanned_exit" ) - def _stamp_exit_communications(self): - """Stamp the still-pending contracts of these (sent or cancelled) exit - communications and run their deferred invoice cleanup. + def _finalize_exit_communications(self): + """Finalize these resolved (sent or canceled) exit communications: + record them as sent on their contracts, and run the deferred invoice + cleanup on the ones already terminated. """ if not self: return - # Only pending contracts: skips ones already handled (re-send, duplicate - # job, or the second leg of a "both" communication). - pending = self.get_objects().filtered("exit_communication_pending") - if pending: - pending.write({"exit_communication_sent": fields.Datetime.now()}) - pending.cancel_contract_invoices() + contracts = self.get_objects() + # Cleanup only the terminated departures still waiting for it. + to_clean = contracts.filtered("exit_communication_pending") + # Stamp all, incl. still-active contracts, so a later termination does + # not re-flag the sponsorship as awaiting the communication. + contracts.write({"exit_communication_sent": fields.Datetime.now()}) + to_clean.cancel_contract_invoices() def _job_sent(self, send_mode): res = super()._job_sent(send_mode) - # Set exit communications (only) to completed + # Finalize (only) exit communications now that they have actually been sent. exit_confs = self._exit_communication_configs() - self.filtered(lambda j: j.config_id in exit_confs)._stamp_exit_communications() + self.filtered( + lambda j: j.config_id in exit_confs + )._finalize_exit_communications() return res From 4929a41facb5708b974af2001de7660c24b1094f Mon Sep 17 00:00:00 2001 From: Daniel Gergely Date: Tue, 23 Jun 2026 08:38:34 +0200 Subject: [PATCH 5/5] FIX: greptile comment No. 10052342... --- .../models/partner_communication.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/partner_communication_compassion/models/partner_communication.py b/partner_communication_compassion/models/partner_communication.py index 39568e4cd..e5f04e5f0 100644 --- a/partner_communication_compassion/models/partner_communication.py +++ b/partner_communication_compassion/models/partner_communication.py @@ -196,9 +196,12 @@ def _finalize_exit_communications(self): contracts = self.get_objects() # Cleanup only the terminated departures still waiting for it. to_clean = contracts.filtered("exit_communication_pending") - # Stamp all, incl. still-active contracts, so a later termination does - # not re-flag the sponsorship as awaiting the communication. - contracts.write({"exit_communication_sent": fields.Datetime.now()}) + # Record contracts not yet stamped (incl. ones still active, so a later + # termination does not re-flag them); skip already-recorded ones to keep + # the original communication date on a re-send. + contracts.filtered(lambda c: not c.exit_communication_sent).write( + {"exit_communication_sent": fields.Datetime.now()} + ) to_clean.cancel_contract_invoices() def _job_sent(self, send_mode):