@@ -62,11 +62,14 @@ def test_button_queues_extraction(self):
6262 """Button should set status='processing' + queued_at when async enabled."""
6363 self .ICP .set_param (self ._p + 'ai_async_extraction' , 'True' )
6464 move = self ._create_invoice_with_attachment ()
65+ attachment = move .attachment_ids .filtered (lambda att : att .mimetype == 'application/pdf' )[:1 ]
6566
6667 result = move .action_ai_extract ()
6768
6869 self .assertEqual (move .ai_extraction_status , 'processing' )
6970 self .assertTrue (move .ai_extraction_queued_at )
71+ self .assertEqual (move .ai_queued_attachment_id , attachment )
72+ self .assertFalse (move .ai_last_extraction_attachment_id )
7073 self .assertEqual (result ['tag' ], 'display_notification' )
7174
7275 def test_cron_processes_queue (self ):
@@ -85,6 +88,39 @@ def test_cron_processes_queue(self):
8588 self .assertEqual (move .ai_extraction_status , 'done' )
8689 self .assertTrue (move .ai_last_extraction_data )
8790 self .assertFalse (move .ai_extraction_queued_at )
91+ self .assertFalse (move .ai_queued_attachment_id )
92+
93+ def test_cron_uses_scheduled_attachment (self ):
94+ """Cron should process the attachment that was queued, not rediscover a newer one."""
95+ self .ICP .set_param (self ._p + 'ai_async_extraction' , 'True' )
96+ move = self ._create_invoice_with_attachment ()
97+ queued_attachment = move .attachment_ids .filtered (lambda att : att .mimetype == 'application/pdf' )[:1 ]
98+ self .env ['ir.attachment' ].create (
99+ {
100+ 'name' : 'later.png' ,
101+ 'datas' : base64 .b64encode (b'fake-image' ),
102+ 'res_model' : 'account.move' ,
103+ 'res_id' : move .id ,
104+ 'mimetype' : 'image/png' ,
105+ }
106+ )
107+ move .ai_extraction_status = 'processing'
108+ move .ai_extraction_queued_at = fields .Datetime .now ()
109+ move .ai_queued_attachment_id = queued_attachment .id
110+
111+ def _mock_extract (api_key , attachment , preview = False ):
112+ self .assertEqual (attachment , queued_attachment )
113+ return self .mock_data
114+
115+ with patch .object (
116+ type (move ),
117+ '_ai_trigger_extraction' ,
118+ side_effect = _mock_extract ,
119+ ), patch .object (self .env .cr , 'commit' ):
120+ self .env ['account.move' ]._ai_cron_process_queue ()
121+
122+ self .assertEqual (move .ai_last_extraction_attachment_id , queued_attachment )
123+ self .assertFalse (move .ai_queued_attachment_id )
88124
89125 def test_cron_handles_failure (self ):
90126 """Cron should set status='failed' when extraction returns None."""
@@ -101,6 +137,7 @@ def test_cron_handles_failure(self):
101137
102138 self .assertEqual (move .ai_extraction_status , 'failed' )
103139 self .assertFalse (move .ai_extraction_queued_at )
140+ self .assertFalse (move .ai_queued_attachment_id )
104141
105142 def test_sync_fallback (self ):
106143 """When async is disabled, extraction should happen synchronously."""
@@ -144,14 +181,17 @@ def test_cron_marks_stale_as_failed(self):
144181 """Items queued > 10 minutes ago should be marked as failed."""
145182 self .ICP .set_param (self ._p + 'ai_async_extraction' , 'True' )
146183 move = self ._create_invoice_with_attachment ()
184+ queued_attachment = move .attachment_ids .filtered (lambda att : att .mimetype == 'application/pdf' )[:1 ]
147185 move .ai_extraction_status = 'processing'
148186 move .ai_extraction_queued_at = fields .Datetime .now () - timedelta (minutes = 15 )
187+ move .ai_queued_attachment_id = queued_attachment .id
149188
150189 with patch .object (self .env .cr , 'commit' ):
151190 self .env ['account.move' ]._ai_cron_process_queue ()
152191
153192 self .assertEqual (move .ai_extraction_status , 'failed' )
154193 self .assertFalse (move .ai_extraction_queued_at )
194+ self .assertFalse (move .ai_queued_attachment_id )
155195
156196 def test_cron_batch_size_limit (self ):
157197 """Cron should only process up to 5 items per run."""
0 commit comments