Project

General

Profile

Download (21.6 KB) Statistics
| Branch: | Tag: | Revision:

calebasse / calebasse / facturation / models.py @ c94d6cba

1
# -*- coding: utf-8 -*-
2

    
3
from datetime import date, datetime
4
from dateutil.relativedelta import relativedelta
5

    
6
from django.db import models
7
from django.db.models import Max
8

    
9
from model_utils import Choices
10

    
11
from calebasse.ressources.models import ServiceLinkedManager
12

    
13
import list_acts
14

    
15
def quarter_start_and_end_dates(today=None):
16
    '''Returns the first and last day of the current quarter'''
17
    if today is None:
18
        today = date.today()
19
    quarter = today.month / 3
20
    start_date = date(day=1, month=(quarter*3)+1, year=today.year)
21
    end_date = start_date + relativedelta(months=3) + relativedelta(days=-1)
22
    return start_date, end_date
23

    
24
class InvoicingManager(ServiceLinkedManager):
25
    def current_for_service(self, service):
26
        '''Return the currently open invoicing'''
27
        if service.name != 'CMPP':
28
            start_date, end_date = quarter_start_and_end_dates()
29
            invoicing, created = \
30
                self.get_or_create(start_date=start_date,
31
                end_date=end_date, service=service)
32
            if created:
33
                invoicing.status = Invoicing.STATUS.closed
34
                invoicing.save()
35
        else:
36
            try:
37
                invoicing = self.get(service=service,
38
                        status=Invoicing.STATUS.open)
39
            except Invoicing.DoesNotExist:
40
                today = date.today()
41
                start_date = date(day=today.day, month=today.month, year=today.year)
42
                invoicing, created = self.get_or_create(service=service,
43
                    start_date=start_date,
44
                    status=Invoicing.STATUS.open)
45
        return invoicing
46

    
47
    def last_for_service(self, service):
48
        current = self.current_for_service(service)
49
        last_seq_id = current.seq_id - 1
50
        try:
51
            return self.get(service=service,
52
                seq_id=last_seq_id)
53
        except:
54
            return None
55

    
56

    
57
def build_invoices_from_acts(acts_diagnostic, acts_treatment):
58
    invoices = {}
59
    len_invoices = 0
60
    len_invoices_hors_pause = 0
61
    len_acts_invoiced = 0
62
    len_acts_invoiced_hors_pause = 0
63
    for patient, acts in acts_diagnostic.items():
64
        invoices[patient] = []
65
        act, hc = acts[0]
66
        invoice = {}
67
        invoice['ppa'] = PricePerAct.get_price(act.date)
68
        invoice['year'] = act.date.year
69
        invoice['acts'] = [(act, hc)]
70
        for act, hc in acts[1:]:
71
            if invoice['ppa'] != PricePerAct.get_price(act.date) or \
72
                    invoice['year'] != act.date.year:
73
                invoices[patient].append(invoice)
74
                len_invoices = len_invoices + 1
75
                len_acts_invoiced = len_acts_invoiced + len(invoice['acts'])
76
                if not patient.pause:
77
                    len_invoices_hors_pause = len_invoices_hors_pause + 1
78
                    len_acts_invoiced_hors_pause = len_acts_invoiced_hors_pause + len(invoice['acts'])
79
                invoice['ppa'] = PricePerAct.get_price(act.date)
80
                invoice['year'] = act.date.year
81
                invoice['acts'] = [(act, hc)]
82
            else:
83
                invoice['acts'].append((act, hc))
84
        invoices[patient].append(invoice)
85
        len_invoices = len_invoices + 1
86
        len_acts_invoiced = len_acts_invoiced + len(invoice['acts'])
87
        if not patient.pause:
88
            len_invoices_hors_pause = len_invoices_hors_pause + 1
89
            len_acts_invoiced_hors_pause = len_acts_invoiced_hors_pause + len(invoice['acts'])
90
    for patient, acts in acts_treatment.items():
91
        if not patient in invoices:
92
            invoices[patient] = []
93
        act, hc = acts[0]
94
        invoice = {}
95
        invoice['ppa'] = PricePerAct.get_price(act.date)
96
        invoice['year'] = act.date.year
97
        invoice['acts'] = [(act, hc)]
98
        for act, hc in acts[1:]:
99
            if invoice['ppa'] != PricePerAct.get_price(act.date) or \
100
                    invoice['year'] != act.date.year:
101
                invoices[patient].append(invoice)
102
                len_invoices = len_invoices + 1
103
                len_acts_invoiced = len_acts_invoiced + len(invoice['acts'])
104
                if not patient.pause:
105
                    len_invoices_hors_pause = len_invoices_hors_pause + 1
106
                    len_acts_invoiced_hors_pause = len_acts_invoiced_hors_pause + len(invoice['acts'])
107
                invoice['price'] = PricePerAct.get_price(act.date)
108
                invoice['year'] = act.date.year
109
                invoice['acts'] = [(act, hc)]
110
            else:
111
                invoice['acts'].append((act, hc))
112
        invoices[patient].append(invoice)
113
        len_invoices = len_invoices + 1
114
        len_acts_invoiced = len_acts_invoiced + len(invoice['acts'])
115
        if not patient.pause:
116
            len_invoices_hors_pause = len_invoices_hors_pause + 1
117
            len_acts_invoiced_hors_pause = len_acts_invoiced_hors_pause + len(invoice['acts'])
118
    return (invoices, len_invoices, len_invoices_hors_pause,
119
        len_acts_invoiced, len_acts_invoiced_hors_pause)
120

    
121

    
122
class Invoicing(models.Model):
123
    '''Represent a batch of invoices:
124

    
125
       end_date - only acts before this date will be considered
126
       status   - current status of the invoicing
127
       acts     - acts bounded to this invoicing when the invoicing is validated
128

    
129
       STATUS - the possible status:
130
        - open, the invoicing is open for new acts
131
        - closed, invoicing has been closed, no new acts will be accepted after
132
          the end_date,
133
        - validated,
134
    '''
135
    STATUS = Choices('open', 'closed', 'validated', 'sent')
136

    
137
    seq_id = models.IntegerField(blank=True, null=True)
138

    
139
    service = models.ForeignKey('ressources.Service', on_delete='PROTECT')
140

    
141
    start_date = models.DateField(
142
            verbose_name=u'Ouverture de la facturation')
143

    
144
    end_date = models.DateField(
145
            verbose_name=u'Clôturation de la facturation',
146
            blank=True,
147
            null=True)
148

    
149
    status = models.CharField(
150
            verbose_name=u'Statut',
151
            choices=STATUS,
152
            default=STATUS.open,
153
            max_length=20)
154

    
155
    acts = models.ManyToManyField('actes.Act')
156

    
157
    objects = InvoicingManager()
158

    
159
    class Meta:
160
        unique_together = (('seq_id', 'service'),)
161

    
162
    def allocate_seq_id(self):
163
        '''Allocate a new sequence id for a new invoicing.'''
164
        seq_id = 1
165
        max_seq_id = Invoicing.objects.for_service(self.service) \
166
                .aggregate(Max('seq_id'))['seq_id__max']
167
        if max_seq_id:
168
            seq_id = seq_id + max_seq_id
169
        return seq_id
170

    
171
    def list_for_billing(self):
172
        '''Return the acts candidates for billing'''
173
        if self.service.name == 'CMPP':
174
            end_date = self.end_date
175
            if not end_date:
176
                today = date.today()
177
                end_date = date(day=today.day, month=today.month, year=today.year)
178
            return list_acts.list_acts_for_billing_CMPP_2(end_date,
179
                    service=self.service)
180
        elif self.service.name == 'CAMSP':
181
            return list_acts.list_acts_for_billing_CAMSP(self.start_date,
182
                    self.end_date, service=self.service)
183
        elif 'SESSAD' in self.service.name:
184
            return list_acts.list_acts_for_billing_SESSAD(self.start_date,
185
                    self.end_date, service=self.service)
186
        else:
187
            raise RuntimeError('Unknown service', self.service)
188

    
189
    def get_stats_or_validate(self, commit=False):
190
        '''
191
            If the invoicing is in state open or closed and commit is False
192
                Return the stats of the billing
193
            If the invoicing is in state open or closed and commit is True
194
                Proceed to invoices creation, healthcare charging, acts a billed
195
                Return the stats of the billing
196
            If the invoicing is in state validated or sent
197
                Return the stats of the billing
198

    
199
            len_patients: Tous les patients concernés, c'est à dire ayant au moins un acte sur la période, même si absent ou verouillé
200
            len_invoices: Nombre de factures avec les dossiers en pause qui seraient facturés
201
            len_invoices_hors_pause: Nombre de factures sans les dossiers en pause
202
            len_acts_invoiced: Nombre d'actes concernés par les factures avec les dossiers en pause qui seraient facturés
203
            len_acts_invoiced_hors_pause: Nombre d'actes concernés par les factures sans les dossiers en pause
204
            len_patient_invoiced: Nombre de patients concernés par les factures avec les dossiers en pause qui seraient facturés
205
            len_patient_invoiced_hors_pause: Nombre de patients concernés par les factures sans les dossiers en pause
206
            len_acts_lost: Nombre d'actes facturables mais qui ne peuvent être facturés pour une autre raison que la pause.
207
            len_patient_with_lost_acts: Nombre de patients concernés par des actes facturables mais qui ne peuvent être facturés pour une autre raison que la pause.
208
            patients_stats: Par patients, factures et actes facturables qui ne peuvent être facturés pour une autre raison que la pause.
209
        '''
210
        days_not_locked = 0
211
        if self.service.name == 'CMPP':
212
            if self.status in (Invoicing.STATUS.open,
213
                    Invoicing.STATUS.closed):
214
                '''
215
                    The stats are build dynamiccaly according to the
216
                    acts billable and the healthcares
217
                '''
218
                (acts_not_locked, days_not_locked, acts_not_valide,
219
                    acts_not_billable, acts_diagnostic, acts_treatment,
220
                    acts_losts) = self.list_for_billing()
221

    
222
                (invoices, len_invoices, len_invoices_hors_pause,
223
                    len_acts_invoiced, len_acts_invoiced_hors_pause) = \
224
                        build_invoices_from_acts(acts_diagnostic, acts_treatment)
225
                len_patient_invoiced = len(invoices.keys())
226
                len_patient_invoiced_hors_pause = 0
227
                for patient in invoices.keys():
228
                    if not patient.pause:
229
                        len_patient_invoiced_hors_pause = len_patient_invoiced_hors_pause + 1
230

    
231
                patients = set(acts_not_locked.keys() + acts_not_valide.keys() + \
232
                    acts_not_billable.keys() + acts_diagnostic.keys() + acts_treatment.keys() + \
233
                    acts_losts.keys())
234

    
235
                patients_stats = {}
236
                len_patient_with_lost_acts = 0
237
                len_acts_lost = 0
238
                for patient in patients:
239
                    patients_stats[patient] = {}
240
                    if patient in invoices.keys():
241
                        patients_stats[patient]['invoices'] = invoices[patient]
242
                        if commit and not patient.pause:
243
                            for invoice in invoices[patient]:
244
                                ppa = invoice['ppa']
245
                                acts = invoice['acts']
246
                                amount = ppa * len(acts)
247
                                in_o = Invoice(patient=patient,
248
                                    invoicing=self,
249
                                    ppa=invoice['ppa'],
250
                                    amount=amount)
251
                                in_o.save()
252
                                for act, hc in acts:
253
                                    act.is_billed = True
254
                                    act.healthcare = hc
255
                                    act.save()
256
                                    in_o.acts.add(act)
257
                            pass
258
                    if patient in acts_losts.keys():
259
                        # TODO: More details about healthcare
260
                        patients_stats[patient]['losts'] = acts_losts[patient]
261
                        len_patient_with_lost_acts = len_patient_with_lost_acts + 1
262
                        len_acts_lost = len_acts_lost + len(acts_losts[patient])
263
                len_patients = len(patients_stats.keys())
264

    
265
                if commit:
266
                    self.status = Invoicing.STATUS.validated
267
                    self.save()
268

    
269
            else:
270
                '''
271
                    Grab stats from the invoices
272
                '''
273
                len_patients = 0
274
                len_invoices = 0
275
                len_acts_invoiced = 0
276
                patients_stats = {}
277
                days_not_locked = []
278
                invoices = self.invoice_set.all()
279
                for invoice in invoices:
280
                    len_invoices = len_invoices + 1
281
                    len_acts_invoiced = len_acts_invoiced + len(invoice.acts.all())
282
                    if not invoice.patient in patients_stats:
283
                        len_patients = len_patients + 1
284
                        patients_stats[invoice.patient] = {}
285
                        patients_stats[invoice.patient]['invoices'] = [invoice]
286
                    else:
287
                        patients_stats[invoice.patient]['invoices'].append(invoice)
288
                # all patients in the invoicing are invoiced
289
                len_patient_invoiced = 0
290
                # These stats are not necessary because excluded form the validated invoicing
291
                len_invoices_hors_pause = 0
292
                len_acts_invoiced_hors_pause = 0
293
                len_patient_invoiced_hors_pause = 0
294
                len_acts_lost = 0
295
                len_patient_with_lost_acts = 0
296
            return (len_patients, len_invoices, len_invoices_hors_pause,
297
                len_acts_invoiced, len_acts_invoiced_hors_pause,
298
                len_patient_invoiced, len_patient_invoiced_hors_pause,
299
                len_acts_lost, len_patient_with_lost_acts, patients_stats, days_not_locked)
300
        elif self.service.name == 'CAMSP':
301
            if self.status in Invoicing.STATUS.closed:
302
                (acts_not_locked, days_not_locked, acts_not_valide,
303
                acts_not_billable, acts_bad_state,
304
                acts_accepted) = self.list_for_billing()
305
                len_patient_pause = 0
306
                len_patient_hors_pause = 0
307
                len_acts_pause = 0
308
                len_acts_hors_pause = 0
309
                for patient, acts in acts_accepted.items():
310
                    if patient.pause:
311
                        len_patient_pause = len_patient_pause + 1
312
                        len_acts_pause = len_acts_pause + len(acts)
313
                    else:
314
                        len_patient_hors_pause = len_patient_hors_pause + 1
315
                        len_acts_hors_pause = len_acts_hors_pause + len(acts)
316
                        if commit:
317
                            for act in acts:
318
                                self.acts.add(act)
319
                if commit:
320
                    self.status = Invoicing.STATUS.validated
321
                    self.save()
322
            else:
323
                acts_accepted = {}
324
                len_patient_pause = 0
325
                len_patient_hors_pause = 0
326
                len_acts_pause = 0
327
                len_acts_hors_pause = 0
328
                days_not_locked = []
329
                for act in self.acts.all():
330
                    if act.patient in acts_accepted:
331
                        acts_accepted[patient].append(act)
332
                        len_acts_hors_pause = len_acts_hors_pause + 1
333
                    else:
334
                        len_patient_hors_pause = len_patient_hors_pause + 1
335
                        len_acts_hors_pause = len_acts_hors_pause + 1
336
                        acts_accepted[patient] = [act]
337
            return (len_patient_pause, len_patient_hors_pause,
338
                len_acts_pause, len_acts_hors_pause, acts_accepted,
339
                days_not_locked)
340
        else:
341
            if self.status in Invoicing.STATUS.closed:
342
                (acts_not_locked, days_not_locked, acts_not_valide,
343
                acts_not_billable, acts_bad_state, acts_missing_valid_notification,
344
                acts_accepted) = self.list_for_billing()
345
                len_patient_pause = 0
346
                len_patient_hors_pause = 0
347
                len_acts_pause = 0
348
                len_acts_hors_pause = 0
349
                for patient, acts in acts_accepted.items():
350
                    if patient.pause:
351
                        len_patient_pause = len_patient_pause + 1
352
                        len_acts_pause = len_acts_pause + len(acts)
353
                    else:
354
                        len_patient_hors_pause = len_patient_hors_pause + 1
355
                        len_acts_hors_pause = len_acts_hors_pause + len(acts)
356
                        if commit:
357
                            for act in acts:
358
                                self.acts.add(act)
359
                len_patient_missing_notif = 0
360
                len_acts_missing_notif = 0
361
                for patient, acts in acts_missing_valid_notification.items():
362
                    len_patient_missing_notif = len_patient_hors_pause + 1
363
                    len_acts_missing_notif = len_acts_missing_notif + len(acts)
364
                if commit:
365
                    self.status = Invoicing.STATUS.validated
366
                    self.save()
367
            else:
368
                acts_accepted = {}
369
                len_patient_pause = 0
370
                len_patient_hors_pause = 0
371
                len_acts_pause = 0
372
                len_acts_hors_pause = 0
373
                len_patient_missing_notif = 0
374
                len_acts_missing_notif = 0
375
                days_not_locked = []
376
                for act in self.acts.all():
377
                    if act.patient in acts_accepted:
378
                        acts_accepted[patient].append(act)
379
                        len_acts_hors_pause = len_acts_hors_pause + 1
380
                    else:
381
                        len_patient_hors_pause = len_patient_hors_pause + 1
382
                        len_acts_hors_pause = len_acts_hors_pause + 1
383
                        acts_accepted[patient] = [act]
384
            return (len_patient_pause, len_patient_hors_pause,
385
                len_acts_pause, len_acts_hors_pause,
386
                len_patient_missing_notif, len_acts_missing_notif,
387
                acts_accepted, days_not_locked)
388

    
389
    def save(self, *args, **kwargs):
390
        if not self.seq_id:
391
            self.seq_id = self.allocate_seq_id()
392
        super(Invoicing, self).save(*args, **kwargs)
393

    
394
    def close(self, end_date=None):
395
        '''Close an open invoicing'''
396
        if self.service.name != 'CMPP':
397
            raise RuntimeError('closing Invoicing is only for the CMPP')
398
        if self.status != Invoicing.STATUS.open:
399
            raise RuntimeError('closing an un-opened Invoicing')
400
        if not end_date:
401
            today = date.today()
402
            end_date = date(day=today.day, month=today.month, year=today.year)
403
            if end_date < self.start_date:
404
                end_date = self.start_date + relativedelta(days=1)
405
        self.status = Invoicing.STATUS.closed
406
        self.end_date = end_date
407
        self.save()
408
        start_date = self.end_date + relativedelta(days=1)
409
        invoicing = Invoicing(service=self.service,
410
            start_date=start_date,
411
            status=Invoicing.STATUS.open)
412
        invoicing.save()
413
        return invoicing
414

    
415

    
416
class PricePerAct(models.Model):
417
    price = models.IntegerField()
418
    start_date = models.DateField(
419
            verbose_name=u"Prise d'effet",
420
            default=date(day=5,month=1,year=1970))
421
    end_date = models.DateField(
422
            verbose_name=u"Fin d'effet",
423
            blank=True,
424
            null=True)
425

    
426
    @classmethod
427
    def get_price(cls, at_date=None):
428
        if not at_date:
429
            at_date = date.today()
430
        if isinstance(at_date, datetime):
431
            at_date = date(day=at_date.day, month=at_date.month,
432
                year=at_date.year)
433
        found = cls.objects.filter(start_date__lte = at_date).latest('start_date')
434
        if not found:
435
            raise Exception('No price to apply')
436
        return found.price
437

    
438
    def __unicode__(self):
439
        val = str(self.price) + ' from ' + str(self.start_date)
440
        if self.end_date:
441
            val = val + ' to ' + str(self.end_date)
442
        return val
443

    
444

    
445
def add_price(price, start_date=None):
446
    price_o = None
447
    ppas = PricePerAct.objects.all()
448
    if ppas:
449
        if not start_date:
450
            raise Exception('A start date is mandatory to add a new Price')
451
        last_ppa = PricePerAct.objects.latest('start_date')
452
        if last_ppa.start_date >= start_date:
453
            raise Exception('The new price cannot apply before the price that currently applies.')
454
        if last_ppa.end_date and last_ppa.end_date != (start_date + relativedelta(days=-1)):
455
            raise Exception('The new price should apply the day after the last price ends.')
456
        last_ppa.end_date = start_date + relativedelta(days=-1)
457
        last_ppa.save()
458
    if not start_date:
459
        price_o = PricePerAct(price=price)
460
    else:
461
        price_o = PricePerAct(price=price, start_date=start_date)
462
    price_o.save()
463
    return price_o
464

    
465

    
466
# Last invoice from the previous software
467
INVOICE_OFFSET = 10000
468

    
469

    
470
class Invoice(models.Model):
471
    number = models.IntegerField(blank=True, null=True)
472
    created = models.DateTimeField(u'Création', auto_now_add=True)
473
    patient = models.ForeignKey('dossiers.PatientRecord')
474
    invoicing = models.ForeignKey('facturation.Invoicing',
475
        on_delete='PROTECT')
476
    acts = models.ManyToManyField('actes.Act')
477
    amount = models.IntegerField()
478
    ppa = models.IntegerField()
479

    
480
    def allocate_number(self):
481
        number = 1
482
        max_number = Invoice.objects.all() \
483
                .aggregate(Max('number'))['number__max']
484
        if max_number:
485
            number = number + max_number
486
        number = number + INVOICE_OFFSET
487
        return number
488

    
489
    def save(self, *args, **kwargs):
490
        if not self.number:
491
            self.number = self.allocate_number()
492
        super(Invoice, self).save(*args, **kwargs)
493

    
494
    def __unicode__(self):
495
        return "Invoice n %d of %d euros for %d acts" % (self.number, self.amount, len(self.acts.all()))
(5-5/8)