@@ -217,6 +217,86 @@ def copy(self, exclude_fields=["id", "uuid"]):
217217
218218 return new_instance
219219
220+ @classmethod
221+ def bulk_save (cls , data_list , user , batch_size = 100 ):
222+ """
223+ Efficiently update or create multiple instances based on 'id' field.
224+ All operations are atomic - either all succeed or all fail.
225+
226+ Args:
227+ data_list: List of dicts with instance data (with or without 'id')
228+ user: User performing the operation
229+ batch_size: Number of records to process per batch
230+
231+ Returns:
232+ dict with 'created' and 'updated' counts
233+ """
234+ from django .db import transaction
235+
236+ if not data_list :
237+ return {'created' : 0 , 'updated' : 0 }
238+
239+ now = py_datetime .now ()
240+
241+ ids_to_update = [d ['id' ] for d in data_list if d .get ('id' )]
242+
243+ existing = {obj .id : obj for obj in cls .objects .filter (id__in = ids_to_update , is_deleted = False )}
244+
245+ to_create = []
246+ to_update = []
247+
248+ exclude_fields = {'id' , 'uuid' , 'date_created' , 'user_created' , 'date_updated' ,
249+ 'user_updated' , 'version' , 'is_deleted' , 'date_valid_from' ,
250+ 'date_valid_to' , 'replacement_uuid' }
251+
252+ for data in data_list :
253+ record_id = data .get ('id' )
254+
255+ if record_id and record_id in existing :
256+ instance = existing [record_id ]
257+ for field , value in data .items ():
258+ if field not in exclude_fields :
259+ setattr (instance , field , value )
260+ instance .user_updated = user
261+ instance .date_updated = now
262+ instance .version = F ('version' ) + 1
263+ to_update .append (instance )
264+ else :
265+ create_data = {k : v for k , v in data .items () if k not in exclude_fields }
266+ instance = cls (** create_data )
267+ instance .set_pk ()
268+ instance .user_created = user
269+ instance .user_updated = user
270+ instance .date_created = now
271+ instance .date_updated = now
272+ instance .version = 1
273+ to_create .append (instance )
274+
275+ with transaction .atomic ():
276+ created_count = 0
277+ updated_count = 0
278+
279+ if to_create :
280+ cls .objects .bulk_create (to_create , batch_size = batch_size )
281+ created_count = len (to_create )
282+
283+ if to_update :
284+ update_fields = [f for f in to_update [0 ].__dict__ .keys ()
285+ if not f .startswith ('_' ) and f not in exclude_fields ]
286+ update_fields += ['user_updated' , 'date_updated' , 'version' ]
287+
288+ cls .objects .bulk_update (to_update , update_fields , batch_size = batch_size )
289+
290+ ids = [obj .id for obj in to_update ]
291+ updated_objects = cls .objects .filter (id__in = ids ).only ('id' , 'version' )
292+ version_map = {obj .id : obj .version for obj in updated_objects }
293+ for obj in to_update :
294+ obj .version = version_map .get (obj .id , obj .version )
295+
296+ updated_count = len (to_update )
297+
298+ return {'created' : created_count , 'updated' : updated_count }
299+
220300 @classmethod
221301 def filter_queryset (cls , queryset = None ):
222302 if queryset is None :
0 commit comments