~taktik/openobject-addons/hui-extra-6.1

« back to all changes in this revision

Viewing changes to contract_base/contract.py

  • Committer: openerp
  • Date: 2013-07-03 19:40:45 UTC
  • Revision ID: openerp@oerp61-20130703194045-ag6gx7w0zbphr9x5
update noviat 6.1 modules

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- encoding: utf-8 -*-
 
2
##############################################################################
 
3
#
 
4
#    OpenERP, Open Source Management Solution
 
5
#
 
6
#    Copyright (c) 2013 Noviat nv/sa (www.noviat.be). All rights reserved.
 
7
#
 
8
#    This program is free software: you can redistribute it and/or modify
 
9
#    it under the terms of the GNU Affero General Public License as
 
10
#    published by the Free Software Foundation, either version 3 of the
 
11
#    License, or (at your option) any later version.
 
12
#
 
13
#    This program is distributed in the hope that it will be useful,
 
14
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
#    GNU Affero General Public License for more details.
 
17
#
 
18
#    You should have received a copy of the GNU Affero General Public License
 
19
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
20
#
 
21
##############################################################################
 
22
 
 
23
from datetime import datetime, date, timedelta
 
24
from dateutil.relativedelta import relativedelta
 
25
from osv import osv, fields
 
26
from tools.translate import translate, _
 
27
from decimal_precision import decimal_precision as dp
 
28
import netsvc
 
29
import logging
 
30
_logger = logging.getLogger(__name__)
 
31
 
 
32
class contract_category(osv.osv):
 
33
    _name = 'contract.category'
 
34
    _description = 'Contract Category'
 
35
    _order = 'code'
 
36
    _columns = {
 
37
        'name': fields.char('Name', size=32, required=True),
 
38
        'code': fields.char('Code', size=12, required=True),
 
39
        'company_id': fields.many2one('res.company', 'Company'),
 
40
        'active': fields.boolean('Active'),
 
41
    }
 
42
    _defaults = {
 
43
        'company_id': lambda s, cr, uid, ctx: s.pool.get('res.company')._company_default_get(cr, uid, 'account.account', context=ctx),
 
44
        'active': True,
 
45
    }
 
46
    _sql_constraints = [
 
47
        ('code', 'unique (code)', 'The code must be unique !')
 
48
    ]
 
49
contract_category()
 
50
 
 
51
class contract_document(osv.osv):
 
52
    _name = 'contract.document'
 
53
    _description = 'Contracts'
 
54
    _order = 'name'
 
55
 
 
56
    def _get_contract_ref(self, cr, uid, context=None):
 
57
        if context.get('default_type') == 'sale':
 
58
            return self.pool.get('ir.sequence').next_by_code(cr,uid,'customer.contract.sequence')
 
59
        elif context.get('default_type') == 'purchase':
 
60
            return self.pool.get('ir.sequence').next_by_code(cr,uid,'supplier.contract.sequence')
 
61
        else:
 
62
            return False
 
63
    
 
64
    def _get_contract_type(self, cr, uid, context=None):
 
65
        return [('sale', 'Sale'), ('purchase', 'Purchase')] 
 
66
 
 
67
    def _get_company(self, cr, uid, context=None):
 
68
        if context is None:
 
69
            context = {}
 
70
        user = self.pool.get('res.users').browse(cr, uid, uid, context)
 
71
        company_id = context.get('company_id', user.company_id.id)
 
72
        return company_id
 
73
 
 
74
    def _get_journal(self, cr, uid, context=None):
 
75
        if context is None:
 
76
            context = {}
 
77
        company_id = self._get_company(cr, uid, context)
 
78
        type = context.get('default_type', 'sale')
 
79
        res = self.pool.get('account.journal').search(cr, uid, 
 
80
            [('type', '=', type), ('company_id', '=', company_id)], limit=1)
 
81
        return res and res[0] or False
 
82
 
 
83
    def _get_currency(self, cr, uid, context=None):
 
84
        res = False
 
85
        journal_id = self._get_journal(cr, uid, context=context)
 
86
        if journal_id:
 
87
            journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
 
88
            res = journal.currency and journal.currency.id or journal.company_id.currency_id.id
 
89
        return res
 
90
 
 
91
    def _get_invoices(self, cr, uid, ids, name, args, context=None):
 
92
        result = {}
 
93
        for contract in self.browse(cr, uid, ids, context=context):
 
94
            inv_ids = []
 
95
            for line in contract.contract_line:
 
96
                for billing in line.billing_ids:
 
97
                    if billing.invoice_id:
 
98
                        inv_ids.append(billing.invoice_id.id)
 
99
            inv_ids = list(set(inv_ids))    
 
100
            inv_ids.sort(reverse=True)
 
101
            result[contract.id] = inv_ids
 
102
        return result
 
103
 
 
104
    _columns = {
 
105
        'name': fields.char('Contract Reference', size=128, required=True,
 
106
            readonly=True, states={'draft':[('readonly',False)]}),
 
107
        'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic account', 
 
108
            domain=[('type','!=','view'),('parent_id', '!=', False)],
 
109
            readonly=True, states={'draft':[('readonly',False)]}),
 
110
        'categ_id': fields.many2one('contract.category', 'Contract Category',
 
111
            readonly=True, states={'draft':[('readonly',False)]}),
 
112
        'parent_id': fields.many2one('contract.document', 'Parent Contract',
 
113
                readonly=True, states={'draft':[('readonly',False)]}),
 
114
        'child_ids': fields.one2many('contract.document', 'parent_id', 'Child Contracts'),
 
115
        'user_id': fields.many2one('res.users', 'Contract Owner', required=True,
 
116
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}),
 
117
        'currency_id': fields.many2one('res.currency', 'Currency', required=True, 
 
118
            readonly=True, states={'draft':[('readonly',False)]}),
 
119
        'journal_id': fields.many2one('account.journal', 'Journal', required=True, 
 
120
            readonly=True, states={'draft':[('readonly',False)]},
 
121
            help="Journal for invoices."),
 
122
        'type': fields.selection(_get_contract_type, 'Contract Type', required=True,
 
123
            readonly=True, states={'draft':[('readonly',False)]}),                                 
 
124
        'state': fields.selection([
 
125
            ('draft','Draft'),
 
126
            ('active','Active'),
 
127
            ('end','Terminated'),
 
128
            ('cancel', 'Cancelled'),
 
129
            ], 'State', required=True, readonly=True),    
 
130
        'date_start': fields.date('Date Start', required=True,
 
131
            readonly=True, states={'draft':[('readonly',False)]}),
 
132
        'date_end': fields.date('Date End',
 
133
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}),                                
 
134
        'partner_id': fields.many2one('res.partner', 'Partner', readonly=True, 
 
135
            required=True, states={'draft':[('readonly',False)]}),
 
136
        'address_contact_id': fields.many2one('res.partner.address', 'Contact Address',
 
137
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}),
 
138
        'address_invoice_id': fields.many2one('res.partner.address', 'Invoice Address',
 
139
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}), 
 
140
        'payment_term': fields.many2one('account.payment.term', 'Payment Term',
 
141
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}),
 
142
        'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position',
 
143
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}),
 
144
        'invoice_ids': fields.function(_get_invoices, relation='account.invoice', type="many2many", string='Invoices',
 
145
            help="This is the list of invoices that are attached to contract lines."),
 
146
        'partner_ref': fields.char('Partner Contract Reference', size=64,
 
147
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]},
 
148
            help="You can use this field to record the reference assigned by your Supplier/Customer to this contract"),                
 
149
        'note': fields.text('Notes'),
 
150
        'contract_line': fields.one2many('contract.line', 'contract_id', 'Contract Lines', 
 
151
            readonly=True, states={'draft': [('readonly', False)],'active':[('readonly',False)]}),
 
152
        'related_ids': fields.one2many('contract.document.related', 'contract_id', 'Related Contract Documents'),
 
153
        'company_id': fields.many2one('res.company', 'Company', required=True,
 
154
            readonly=True, states={'draft':[('readonly',False)],'active':[('readonly',False)]}),
 
155
        'active': fields.boolean('Active',
 
156
            readonly=True, states={'draft':[('readonly',False)]}),
 
157
    }
 
158
    _defaults = {
 
159
        'name': _get_contract_ref,
 
160
        'user_id': lambda s, cr, uid, ctx: uid,
 
161
        'type': 'sale',
 
162
        'state': 'draft',
 
163
        'company_id': _get_company,
 
164
        'journal_id': _get_journal,
 
165
        'currency_id': _get_currency,
 
166
        'active': True,
 
167
    }
 
168
 
 
169
    def onchange_partner_id(self, cr, uid, ids, partner_id):
 
170
        invoice_addr_id = False
 
171
        contact_addr_id = False
 
172
        partner_payment_term = False
 
173
        fiscal_position = False
 
174
 
 
175
        addresses = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['contact', 'invoice'])
 
176
        contact_addr_id = addresses['contact']
 
177
        invoice_addr_id = addresses['invoice']
 
178
        partner = self.pool.get('res.partner').browse(cr, uid, partner_id)
 
179
        fiscal_position = partner.property_account_position and partner.property_account_position.id or False
 
180
        partner_payment_term = partner.property_payment_term and partner.property_payment_term.id or False
 
181
 
 
182
        result = {
 
183
            'address_contact_id': contact_addr_id,
 
184
            'address_invoice_id': invoice_addr_id,
 
185
            'payment_term': partner_payment_term,
 
186
            'fiscal_position': fiscal_position
 
187
        }        
 
188
        return {'value': result}
 
189
 
 
190
    def action_confirm(self, cr, uid, ids, context):  
 
191
        return self.write(cr, uid, ids, {'state':'active'}, context=context)
 
192
 
 
193
    def action_end(self, cr, uid, ids, context):  
 
194
        return self.write(cr, uid, ids, {'state':'end'}, context=context)
 
195
 
 
196
    def action_cancel(self, cr, uid, ids, context):  
 
197
        return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
 
198
 
 
199
    def action_draft(self, cr, uid, ids, context):  
 
200
        return self.write(cr, uid, ids, {'state':'draft'}, context=context)
 
201
 
 
202
    def _get_invoice_type(self, cr, uid, contract_type, context=None):
 
203
        if contract_type == 'sale':
 
204
            return 'out_invoice'
 
205
        elif contract_type == 'purchase':
 
206
            return 'in_invoice'
 
207
        else:
 
208
            raise NotImplementedError("Contract Type %s is not supported" % contract_type)
 
209
 
 
210
    def create_invoice(self, cr, uid, ids, period_id=None, date_invoice=None, context=None):
 
211
        #_logger.warn('create_invoice, ids=%s, period_id=%s', ids, period_id)
 
212
        wf_service = netsvc.LocalService('workflow')  
 
213
        cl_obj = self.pool.get('contract.line')
 
214
        clb_obj = self.pool.get('contract.line.billing')
 
215
        inv_obj = self.pool.get('account.invoice')
 
216
        inv_line_obj = self.pool.get('account.invoice.line')
 
217
        inv_line_print_obj = self.pool.get('account.invoice.line.print')
 
218
        
 
219
        for contract in self.browse(cr, uid, ids, context=context):
 
220
            if contract.state != 'active':
 
221
                continue
 
222
            
 
223
            lang = contract.partner_id.lang     
 
224
            def xlat(src):
 
225
                return translate(cr, 'contract.py', 'code', lang, src) or src
 
226
            
 
227
            invoices = {}
 
228
            for cl in contract.contract_line:
 
229
                if cl.billing_result == 'none':
 
230
                    continue
 
231
                if cl.type == 'heading':
 
232
                    continue
 
233
                billings = cl.billing_ids
 
234
                clb_table = cl_obj.calc_billing_table(cr, uid, [cl.id], context=context)
 
235
                for entry in clb_table:
 
236
                    # check if already entry in billings
 
237
                    matches = filter(lambda x: x['date'] == entry['date'] , billings)
 
238
                    if matches:
 
239
                        billed = filter(lambda x: x['billed'], matches)
 
240
                        if not billed:
 
241
                            if len(matches) > 1:
 
242
                                raise osv.except_osv(_('Error !'), _('Ambiguous billing table !'))
 
243
                            else:
 
244
                                if invoices.get(entry['date']):
 
245
                                    invoices[entry['date']] += [{'contract_line': cl, 'billing_id': matches[0].id, 'service_period': entry.get('service_period')}]
 
246
                                else:
 
247
                                    invoices[entry['date']] = [{'contract_line': cl, 'billing_id': matches[0].id, 'service_period': entry.get('service_period')}]
 
248
                    # else create entry
 
249
                    else:
 
250
                        clb_id = clb_obj.create(cr, uid, entry)                       
 
251
                        if invoices.get(entry['date']):
 
252
                            invoices[entry['date']] += [{'contract_line': cl, 'billing_id': clb_id, 'service_period': entry.get('service_period')}]
 
253
                        else:
 
254
                            invoices[entry['date']] = [{'contract_line': cl, 'billing_id': clb_id, 'service_period': entry.get('service_period')}]
 
255
        
 
256
            #_logger.warn('create_invoice, invoices=%s', invoices)
 
257
 
 
258
            for k,v in invoices.iteritems():
 
259
                """
 
260
                An invoice groups all invoice lines with the same billing date. 
 
261
                The invoice date is not specified which implies that the invoice 'confirm' date will be used. 
 
262
                """
 
263
 
 
264
                cls = [x['contract_line'] for x in v]
 
265
                billing_results = [cl.billing_result for cl in cls]
 
266
                inv_state = 'draft' in billing_results and 'draft' or 'open'
 
267
                inv_type = self._get_invoice_type(cr, uid, contract.type)
 
268
                inv_vals = inv_obj.onchange_partner_id(cr, uid, ids, inv_type, contract.partner_id.id,
 
269
                    date_invoice=k, payment_term=contract.payment_term, partner_bank_id=False, company_id=contract.company_id.id)['value']
 
270
 
 
271
                #_logger.warn('create_invoice, inv_vals=%s', inv_vals)
 
272
                inv_vals.update({
 
273
                    'name': contract.categ_id and ', '.join([contract.name, contract.categ_id.code]) or contract.name,
 
274
                    'origin': contract.name,
 
275
                    'type': inv_type,
 
276
                    'date_invoice': date_invoice,
 
277
                    'partner_id': contract.partner_id.id,
 
278
                    'period_id': period_id,
 
279
                    'journal_id': contract.journal_id.id,
 
280
                    'company_id': contract.company_id.id,
 
281
                    'currency_id': contract.currency_id.id,
 
282
                    })
 
283
                #_logger.warn('create_invoice, inv_vals=%s', inv_vals)
 
284
                inv_id = inv_obj.create(cr, uid, inv_vals, context=context)
 
285
                #_logger.warn('create_invoice, inv_id=%s', inv_id)                
 
286
                
 
287
                inv_lines = []            
 
288
                for entry in v:
 
289
                    #_logger.warn('entry = %s', entry)
 
290
                    cl = entry['contract_line']
 
291
                    billing_id = entry['billing_id']
 
292
                    inv_line = {
 
293
                        'contract_line': cl,
 
294
                        'section': cl.parent_id,
 
295
                        'service_period': entry.get('service_period'),
 
296
                    }
 
297
                    inv_lines.append(inv_line)
 
298
                        
 
299
                # group/order lines by section and by service period and insert heading line 
 
300
                # when no corresponding heading line is available for a specific service period
 
301
                #_logger.warn('%s, create_invoice, inv_lines=%s', self._name, inv_lines)
 
302
 
 
303
                # step 1 : add generic 'nrc' and 'rc' sections to lines that don't belong to a section
 
304
                for inv_line in inv_lines:
 
305
                    inv_line['section'] = inv_line['section'] or inv_line['service_period'] and 'rc' or 'nrc'
 
306
                    
 
307
                # step 2 : order lines by 1) section and 2) service period
 
308
                inv_lines.sort(key=lambda k: (k['section'],k['service_period']))
 
309
                
 
310
                #_logger.warn('%s, create_invoice, inv_lines=%s', self._name, inv_lines)
 
311
                # step 3 : add heading lines
 
312
                section_stack = [('empty','empty')]
 
313
                for inv_line in inv_lines:
 
314
                    section = inv_line['section']
 
315
                    service_period = inv_line['service_period']
 
316
                    section_tuple = (section, service_period)
 
317
                    
 
318
                    # create heading line
 
319
                    if section_tuple != section_stack[-1]:
 
320
                        section_stack.append(section_tuple)
 
321
                        if isinstance(section, basestring): 
 
322
                            if section not in ['rc', 'nrc']:
 
323
                                raise NotImplementedError("'%s' : unsupported section !" %section)
 
324
                            cl_child = inv_line['contract_line']
 
325
                            heading_line_vals = {
 
326
                                'invoice_id': inv_id,
 
327
                                'contract_line_id': None,   
 
328
                                'name': section == 'nrc' and xlat('One Time Charges') or xlat('Recurring Charges'),
 
329
                                'sequence': cl_child.sequence, # use sequence number of first child
 
330
                                'type': 'heading',
 
331
                                'service_period_section': service_period,
 
332
                            }
 
333
                        else:
 
334
                            cl = inv_line['section']
 
335
                            heading_line_vals = {
 
336
                                'invoice_id': inv_id,
 
337
                                'contract_line_id': cl.id,   
 
338
                                'name': cl.name,
 
339
                                'sequence': cl.sequence,
 
340
                                'type': 'heading',
 
341
                                'service_period_section': service_period,
 
342
                                'hidden': cl.hidden,
 
343
                                'note': cl.note,
 
344
                            }
 
345
                        heading_line_id = inv_line_print_obj.create(cr, uid, heading_line_vals)
 
346
                        # To DO : add support for multiple levels of heading lines
 
347
                        
 
348
                    # create invoice lines
 
349
                    cl = inv_line['contract_line']    
 
350
                    inv_line_vals = {
 
351
                         'invoice_id': inv_id,
 
352
                         'name': cl.name,
 
353
                         'service_period': inv_line['service_period'],                         
 
354
                         'account_id': cl.account_id.id,
 
355
                         'price_unit': cl.price_unit,
 
356
                         'quantity': cl.quantity,
 
357
                         'note': cl.note,
 
358
                    }
 
359
                    if cl.product_id:
 
360
                        inv_line_vals.update({
 
361
                            'product_id': cl.product_id.id,
 
362
                            'uos_id': cl.uom_id.id,
 
363
                            'price_unit': cl.price_unit,
 
364
                        })
 
365
                    if cl.discount:
 
366
                        inv_line_vals['discount'] = cl.discount
 
367
                    if cl.analytic_account_id:
 
368
                        inv_line_vals['account_analytic_id'] = cl.analytic_account_id.id
 
369
                    if cl.tax_ids:
 
370
                        inv_line_vals['invoice_line_tax_id'] = [(6, 0, [x.id for x in cl.tax_ids])]
 
371
                    #_logger.warn('create_invoice, inv_line_vals=%s', inv_line_vals) 
 
372
                    inv_line_id = inv_line_obj.create(cr, uid, inv_line_vals)
 
373
                    
 
374
                    inv_line_print_vals = {
 
375
                        'invoice_id': inv_id,
 
376
                        'invoice_line_id': inv_line_id,
 
377
                        'contract_line_id': cl.id,
 
378
                        'name': cl.name,
 
379
                        'sequence': cl.sequence,
 
380
                        'type': 'normal',
 
381
                        'parent_id': heading_line_id,
 
382
                        'service_period_section': entry.get('service_period'),
 
383
                        'hidden': cl.hidden,
 
384
                        'note': cl.note,
 
385
                    }
 
386
                    #_logger.warn('create_invoice, inv_line_print_vals=%s', inv_line_print_vals) 
 
387
                    inv_line_print_id = inv_line_print_obj.create(cr, uid, inv_line_print_vals)                        
 
388
 
 
389
                inv_obj.button_compute(cr, uid, [inv_id])
 
390
                clb_obj.write(cr, uid, [billing_id], {'invoice_id': inv_id, 'billed': True})
 
391
                if inv_state == 'open':
 
392
                    wf_service.trg_validate(uid, 'account.invoice', inv_id, 'invoice_open', cr)
 
393
                
 
394
        return True
 
395
 
 
396
contract_document()
 
397
 
 
398
class contract_line(osv.osv):
 
399
    _name = 'contract.line'
 
400
    _description = 'Contract Lines'
 
401
    _order = "contract_id, sequence asc"
 
402
 
 
403
    def _get_contract_line(self, cr, uid, ids, context=None):
 
404
        result = []
 
405
        for cl in self.browse(cr, uid, ids):
 
406
            if cl.type == 'normal':
 
407
                result.append(cl.id)
 
408
            def _parent_ids(rec):
 
409
                res = [rec.id]
 
410
                if rec.parent_id:
 
411
                    res += _parent_ids(rec.parent_id)
 
412
                return res
 
413
            if cl.parent_id:
 
414
                result += _parent_ids(cl.parent_id)
 
415
        return result
 
416
 
 
417
    def _amount_line(self, cr, uid, ids, field_name, arg, context):
 
418
        #_logger.warn('_amount_line, ids=%s, field_name=%s', ids, field_name)
 
419
        res = {}
 
420
        tax_obj = self.pool.get('account.tax')
 
421
        cur_obj = self.pool.get('res.currency')
 
422
        for line in self.browse(cr, uid, ids):
 
423
            subtotal = 0.0
 
424
            if line.type == 'normal':
 
425
                amt_lines = [line]
 
426
            else:
 
427
                amt_lines = line.child_ids
 
428
            total = 0.0
 
429
            total_included = 0.0
 
430
            for l in amt_lines:
 
431
                price = l.price_unit * (1-(l.discount or 0.0)/100.0)
 
432
                taxes = tax_obj.compute_all(cr, uid, l.tax_ids, price, l.quantity, product=l.product_id, 
 
433
                    address_id=l.contract_id.address_invoice_id, partner=l.contract_id.partner_id)
 
434
                total += taxes['total']
 
435
                total_included += taxes['total_included']
 
436
            res[line.id] = total
 
437
            cur = line.contract_id.currency_id
 
438
            res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
 
439
        return res
 
440
    
 
441
    def _calc_billing_end(self, cr, uid, billing_start, billing_period_id, billing_period_nbr):
 
442
        billing_start = datetime.strptime(billing_start, '%Y-%m-%d').date()
 
443
        billing_period = self.pool.get('billing.period').browse(cr, uid, billing_period_id)
 
444
        base_period = billing_period.base_period
 
445
        delta = billing_period.base_period_multiplier * billing_period_nbr
 
446
        if base_period == 'day':
 
447
            billing_end = billing_start + timedelta(delta-1)
 
448
        elif base_period == 'week':
 
449
            billing_end = billing_start + timedelta(7*delta-1)
 
450
        elif base_period == 'month':
 
451
            billing_end = billing_start + relativedelta(months=delta, days=-1)
 
452
        elif base_period == 'year':
 
453
            billing_end = billing_start + relativedelta(years=delta, days=-1)
 
454
        return billing_end.isoformat()
 
455
    
 
456
    def _billing_end(self, cr, uid, ids, field_name, arg, context):
 
457
        res = {}
 
458
        for line in self.browse(cr, uid, ids):
 
459
            if line.billing_type == 'one_time':
 
460
                res[line.id] = line.billing_start
 
461
            else:
 
462
                if line.billing_unlimited:
 
463
                    res[line.id] = False
 
464
                else:
 
465
                    billing_end = self._calc_billing_end(cr, uid, line.billing_start, line.billing_period_id.id, line.billing_period_nbr)
 
466
                    res[line.id] = billing_end
 
467
        return res
 
468
    
 
469
    _columns = {
 
470
        'name': fields.char('Description', size=256, required=True),
 
471
        'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of contract lines."),
 
472
        'contract_id': fields.many2one('contract.document', 'Contract Reference', ondelete='cascade'),
 
473
        'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic account', 
 
474
            domain=[('type','!=','view'),('parent_id', '!=', False)]),
 
475
        'uom_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
 
476
        'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
 
477
        'account_id': fields.many2one('account.account', 'Account', domain=[('type','!=','view'), ('type', '!=', 'closed')], 
 
478
            help="The income or expense account related to the selected product."),
 
479
        'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Account')),
 
480
        'price_subtotal': fields.function(_amount_line, string='Subtotal', type='float',
 
481
            digits_compute= dp.get_precision('Account'), 
 
482
            store={
 
483
                'contract.line': (_get_contract_line, ['price_unit','tax_ids','quantity','discount'], 20),    
 
484
            }),
 
485
        'quantity': fields.float('Quantity', required=True),
 
486
        'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Account')),
 
487
        'tax_ids': fields.many2many('account.tax', string='Taxes'),
 
488
        'type': fields.selection([
 
489
            ('heading','Section Heading'),
 
490
            ('normal','Normal'),
 
491
            ], 'Type', required=True),        
 
492
        'parent_id': fields.many2one('contract.line', 'Section',
 
493
            help="Use this field to order contract lines in sections whereby the parent_id contains section heading info."),
 
494
        'child_ids': fields.one2many('contract.line', 'parent_id', 'Section Lines'),
 
495
        'hidden': fields.boolean('Hidden',
 
496
            help='Use this flag to hide contract lines on the printed Invoice.'),
 
497
        # billing info
 
498
        'billing_type': fields.selection([
 
499
            ('recurring','Recurring Charge'),
 
500
            ('one_time','One Time Charge'),
 
501
            ], 'Billing Type'),  
 
502
        'billing_result': fields.selection([
 
503
            ('open','Open Invoice'),
 
504
            ('draft','Draft Invoice'),
 
505
            ('none','None'),
 
506
            ], 'Invoice State', required=True,
 
507
            help="State of the invoice created by the billing engine."
 
508
                 "\n'Draft' prevails in case of multiple lines with different Invoice States."
 
509
                 "\n'None' is used for invoice lines that are created manually."),  
 
510
        'prepaid': fields.boolean('Prepaid',
 
511
            help="Check this box for prepaid billing."),    
 
512
        'billing_start': fields.date('Billing Start Date'),
 
513
        'billing_end': fields.function(_billing_end, string='Billing End Date', type='date', readonly=True),        
 
514
        'billing_period_id': fields.many2one('billing.period', 'Billing Periodicity'),
 
515
        'billing_unlimited': fields.boolean('Unlimited',
 
516
            help="Check this box for recurring billing with no determined end date."),        
 
517
        'billing_period_nbr': fields.integer('Number of Periods'),
 
518
        'billing_ids': fields.one2many('contract.line.billing', 'contract_line_id', 'Billing History'),        
 
519
        #
 
520
        'note': fields.text('Notes'),
 
521
        'company_id': fields.related('contract_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
 
522
    }
 
523
    _defaults = {
 
524
        'discount': 0.0,
 
525
        'quantity': 1.0,
 
526
        'sequence': 10,
 
527
        'price_unit': 0.0,
 
528
        'type': 'normal',
 
529
        'prepaid': True,
 
530
        'billing_type': 'recurring',
 
531
        'billing_result': 'draft',        
 
532
    }
 
533
    
 
534
    def _check_billing_period_nbr(self, cr, uid, ids, context=None):
 
535
        for line in self.browse(cr, uid, ids, context=context):
 
536
            if not line.billing_unlimited and line.billing_period_nbr <= 0:
 
537
                return False
 
538
        return True
 
539
    
 
540
    _constraints = [
 
541
    (_check_billing_period_nbr, '\nThe number of periods must be greater than zero !', ['billing_period_nbr'])
 
542
    ]
 
543
   
 
544
    def onchange_type(self, cr, uid, ids, type):
 
545
        if type == 'heading':
 
546
            return {'value':{'billing_type': None}}
 
547
        else:
 
548
            return {}
 
549
 
 
550
    def onchange_billing_unlimited(self, cr, uid, ids, billing_unlimited, context=None):
 
551
        if billing_unlimited: 
 
552
            return {'value':{'billing_end': False, 'billing_period_nbr': False}}
 
553
        else:
 
554
            return {}
 
555
 
 
556
    def onchange_billing_end(self, cr, uid, ids, billing_start, billing_period_id, billing_period_nbr, billing_unlimited, context=None):
 
557
        if not billing_unlimited and billing_period_nbr <= 0:
 
558
            raise osv.except_osv(_('Error !'), _('The number of periods must be greater than zero !'))
 
559
        billing_end = self._calc_billing_end(cr, uid, billing_start, billing_period_id, billing_period_nbr)
 
560
        return {'value':{'billing_end': billing_end}}
 
561
 
 
562
    def onchange_product_id(self, cr, uid, ids, product_id, contract_type,  
 
563
        partner_id, company_id, currency_id, fposition_id=False, context=None):
 
564
        #_logger.warn('onchange_product_id, product_id=%s, contract_type=%s, partner_id=%s, company_id=%s, currency_id=%s, fposition_id=%s', 
 
565
        #    product_id, contract_type, partner_id, company_id, currency_id, fposition_id)
 
566
        #_logger.warn('onchange_product_id, context=%s', context)
 
567
        if context is None:
 
568
            context = {}
 
569
        
 
570
        partner_obj = self.pool.get('res.partner')
 
571
        fpos_obj = self.pool.get('account.fiscal.position')
 
572
        product_obj = self.pool.get('product.product')
 
573
        company_obj = self.pool.get('res.company')
 
574
        curr_obj = self.pool.get('res.currency')
 
575
        
 
576
        company_id = company_id if company_id != None else context.get('company_id',False)
 
577
        context.update({'company_id': company_id})
 
578
        if not partner_id:
 
579
            raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !"))
 
580
        if not product_id:
 
581
                return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
 
582
        partner = partner_obj.browse(cr, uid, partner_id, context=context)
 
583
        fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
 
584
 
 
585
        product = product_obj.browse(cr, uid, product_id, context=context)
 
586
        company = company_obj.browse(cr, uid, company_id, context=context)
 
587
        currency = curr_obj.browse(cr, uid, currency_id, context=context)
 
588
        if company.currency_id.id != currency.id:
 
589
            multi_currency = True
 
590
        else:
 
591
            multi_currency = False
 
592
 
 
593
        if contract_type == 'sale':
 
594
            name = product_obj.name_get(cr, uid, [product.id], context=context)[0][1]
 
595
            billing_type = product.product_tmpl_id.billing_type_sale
 
596
            billing_result = product.product_tmpl_id.billing_result_sale
 
597
            billing_period_id = product.product_tmpl_id.billing_period_sale_id.id
 
598
            billing_unlimited = product.product_tmpl_id.billing_unlimited_sale
 
599
            billing_period_nbr = product.product_tmpl_id.billing_period_nbr_sale            
 
600
            if not billing_type:
 
601
                billing_type = product.categ_id.billing_type_sale
 
602
                billing_result = product.categ_id.billing_result_sale
 
603
                billing_period_id = product.categ_id.billing_period_sale_id.id
 
604
                billing_unlimited = product.categ_id.billing_unlimited_sale
 
605
                billing_period_nbr = product.categ_id.billing_period_nbr_sale                
 
606
            price_unit = product.list_price
 
607
            tax_ids = fpos_obj.map_tax(cr, uid, fpos, product.taxes_id)
 
608
            uom_id = product.uos_id.id or product.uom_id.id
 
609
            account_id = product.property_account_income.id
 
610
            if not account_id:
 
611
                account_id = product.categ_id.property_account_income_categ.id
 
612
            
 
613
        else:
 
614
            name = product.partner_ref
 
615
            billing_type = product.product_tmpl_id.billing_type_purchase
 
616
            billing_period_id = product.product_tmpl_id.billing_period_purchase_id.id
 
617
            billing_unlimited = product.product_tmpl_id.billing_unlimited_purchase
 
618
            billing_period_nbr = product.product_tmpl_id.billing_period_nbr_purchase           
 
619
            if not billing_type:
 
620
                billing_type = product.categ_id.billing_type_purchase
 
621
                billing_period_id = product.categ_id.billing_period_purchase_id.id
 
622
                billing_unlimited = product.categ_id.billing_unlimited_purchase
 
623
                billing_period_nbr = product.categ_id.billing_period_nbr_purchase                
 
624
            price_unit = product.standard_price
 
625
            tax_ids = fpos_obj.map_tax(cr, uid, fpos, product.supplier_taxes_id)
 
626
            uom_id = product.uom_id.id
 
627
            account_id = product.property_account_expense.id
 
628
            if not account_id:
 
629
                account_id = product.property_account_expense_categ.id
 
630
        
 
631
        if multi_currency:
 
632
            price_unit = price_unit * currency.rate
 
633
            
 
634
        value = {
 
635
            'name': name,
 
636
            'billing_type': billing_type,
 
637
            'billing_period_id': billing_period_id,
 
638
            'billing_unlimited': billing_unlimited,            
 
639
            'billing_period_nbr': billing_period_nbr,
 
640
            'price_unit': price_unit,
 
641
            'tax_ids': tax_ids,
 
642
            'uom_id': uom_id,
 
643
            'account_id': account_id,            
 
644
        }      
 
645
        if contract_type == 'sale': 
 
646
            value['billing_result'] = billing_result
 
647
        #_logger.warn('onchange_product_id, value=%s', value)
 
648
        return {'value': value}
 
649
 
 
650
    # Set the tax field according to the account and the fiscal position
 
651
    def onchange_account_id(self, cr, uid, ids, product_id, partner_id, contract_type, fposition_id, account_id):
 
652
        if not account_id:
 
653
            return {}
 
654
        unique_tax_ids = []
 
655
        fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
 
656
        account = self.pool.get('account.account').browse(cr, uid, account_id)
 
657
        if not product_id:
 
658
            taxes = account.tax_ids
 
659
            unique_tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
 
660
        else:
 
661
            # force user chosen account in context to allow onchange_product_id()
 
662
            # to fallback to the this accounts in case product has no taxes defined.
 
663
            context = {'account_id': account_id}           
 
664
            company = account.company_id
 
665
            product_change_result = self.onchange_product_id(cr, uid, ids, product_id, contract_type,
 
666
                partner_id, company.id, company.currency_id.id, fposition_id=fposition_id, context=context)
 
667
            if product_change_result and 'value' in product_change_result and 'tax_ids' in product_change_result['value']:
 
668
                unique_tax_ids = product_change_result['value']['tax_ids']
 
669
        return {'value':{'tax_ids': unique_tax_ids}}
 
670
 
 
671
    def calc_billing_table(self, cr, uid, contract_line_ids, context=None):
 
672
        clb_obj = self.pool.get('contract.line.billing')
 
673
        clb_tables = []
 
674
        
 
675
        for cline in self.browse(cr, uid, contract_line_ids, context=context):
 
676
            clb_table = {'contract_line_id': cline.id}
 
677
            billing_start = datetime.strptime(cline.billing_start, '%Y-%m-%d').date()
 
678
            today = date.today()
 
679
            #_logger.warn('calc_billing_table, billing_start=%s, today=%s', billing_start, today)
 
680
 
 
681
            if cline.billing_type == 'one_time':
 
682
                clb_table.update({
 
683
                        'number': 1,
 
684
                        'date': billing_start.isoformat(),
 
685
                    })
 
686
                clb_tables.append(clb_table)
 
687
 
 
688
            elif cline.billing_type == 'recurring':
 
689
                base_period = cline.billing_period_id.base_period
 
690
                multiplier = cline.billing_period_id.base_period_multiplier
 
691
                if base_period == 'day':
 
692
                    number = int((today - billing_start).days/multiplier + 1)
 
693
                elif base_period == 'week':
 
694
                    d_start = billing_start.day
 
695
                    d = today.day
 
696
                    x = 1 if (d_start < d) else 0
 
697
                    number = int((today - billing_start).days/(7*multiplier) + x)
 
698
                elif base_period == 'month':
 
699
                    d_start = billing_start.day
 
700
                    d = today.day
 
701
                    x = 1 if (d_start < d) else 0
 
702
                    number = int((12*(today.year - billing_start.year) + today.month - billing_start.month)/multiplier + x)  
 
703
                elif base_period == 'year':
 
704
                    diff_year = today.year - billing_start.year
 
705
                    current_year_renewal = False
 
706
                    if multiplier <= diff_year:
 
707
                        mod = diff_year % multiplier
 
708
                        if not mod:
 
709
                            current_year_renewal = True
 
710
                    if current_year_renewal:
 
711
                        d_start = billing_start.timetuple().tm_yday
 
712
                        d = today.timetuple().tm_yday
 
713
                        x = 1 if (d_start < d) else 0
 
714
                    else:
 
715
                        x = 1
 
716
                    number = int(diff_year/multiplier + x)
 
717
            
 
718
                for i in range(number):
 
719
                    if base_period == 'day':
 
720
                        billing_date = billing_start + timedelta(i)
 
721
                        if cline.prepaid:
 
722
                            service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + timedelta(multiplier - 1)).isoformat())
 
723
                        else:
 
724
                            service_period = '%s - %s' %((billing_date - timedelta(multiplier)).isoformat(), (billing_date - timedelta(1)).isoformat())
 
725
                    elif base_period == 'week':
 
726
                        billing_date = (billing_start + timedelta(i)*7*multiplier)
 
727
                        if cline.prepaid:
 
728
                            service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + timedelta(7*multiplier - 1)).isoformat())
 
729
                        else:
 
730
                            service_period = '%s - %s' %((billing_date - timedelta(7*multiplier)).isoformat(), (billing_date - timedelta(1)).isoformat())
 
731
                    elif base_period == 'month':
 
732
                        billing_date = billing_start + relativedelta(months=i*multiplier)
 
733
                        if cline.prepaid:
 
734
                            service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + relativedelta(months=multiplier, days= -1)).isoformat())
 
735
                        else:
 
736
                            service_period = '%s - %s' %((billing_date - relativedelta(months=multiplier)).isoformat(), (billing_date - timedelta(1)).isoformat())
 
737
                    elif base_period == 'year':
 
738
                        billing_date = billing_start + relativedelta(years=i*multiplier)
 
739
                        if cline.prepaid:
 
740
                            service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + relativedelta(years=multiplier, days= -1)).isoformat())
 
741
                        else:
 
742
                            service_period = '%s - %s' %((billing_date - relativedelta(years=multiplier)).isoformat(), (billing_date - timedelta(1)).isoformat())
 
743
                        
 
744
                    entry = clb_table.copy()
 
745
                    entry.update({
 
746
                        'number': i+1,
 
747
                        'date': billing_date.isoformat(),
 
748
                        'service_period': service_period,
 
749
                    })
 
750
                    #_logger.warn('clb_table=%s', entry)
 
751
                    clb_tables += [entry]
 
752
 
 
753
        #_logger.warn('calc_billing_table, return clb_tables=%s', clb_tables)
 
754
        return clb_tables 
 
755
 
 
756
    def generate_billing_table(self, cr, uid, ids, context=None):    
 
757
        clb_obj = self.pool.get('contract.line.billing')
 
758
        for cline in self.browse(cr, uid, ids, context=context):
 
759
            billing_start = datetime.strptime(cline.billing_start, '%Y-%m-%d').date()
 
760
            today = date.today()
 
761
            #_logger.warn('generate_billing_table, billing_start=%s, today=%s', billing_start, today)
 
762
            if billing_start > today:
 
763
                raise osv.except_osv(_('Warning !'), _("No entries created since the billing hasn't started yet !"))
 
764
            billing_ids = cline.billing_ids
 
765
            if billing_ids:
 
766
                for billing_id in cline.billing_ids:
 
767
                    if billing_id.billed:
 
768
                        raise osv.except_osv(_('Warning !'), 
 
769
                            _("You cannot regenerate the billing table since some entries have been billed already '"))
 
770
                clb_obj.unlink(cr, uid, [x.id for x in billing_ids])
 
771
            clb_table = self.calc_billing_table(cr, uid, [cline.id], context=context)
 
772
            #_logger.warn('generate_billing_table, clb_table=%s', clb_table)
 
773
            for vals in clb_table:
 
774
                clb_obj.create(cr, uid, vals)
 
775
        return True    
 
776
 
 
777
contract_line()
 
778
 
 
779
class contract_line_billing(osv.osv):
 
780
    _name = 'contract.line.billing'
 
781
    _description = 'Contract Line Billing History'
 
782
    _order = 'number'
 
783
    
 
784
    _columns = {
 
785
        'number': fields.integer('Number'),
 
786
        'date': fields.date('Billing Date', required=True),
 
787
        'contract_line_id': fields.many2one('contract.line', 'Contract Line', ondelete='cascade'),
 
788
        'billed': fields.boolean('Billed'),
 
789
        'invoice_id': fields.many2one('account.invoice', 'Invoice'),
 
790
        'service_period': fields.char('Service Period', size = 23),        
 
791
        'note': fields.text('Notes'),
 
792
    }
 
793
    
 
794
    def onchange_invoice_id(self, cr, uid, ids, invoice_id):
 
795
        #_logger.warn('onchange_invoice_id, invoice_id=%s', invoice_id)
 
796
        return {'value': {'billed': invoice_id and True or False}}
 
797
        
 
798
contract_line_billing()
 
799
 
 
800
class contract_document_related(osv.osv):
 
801
    _name = 'contract.document.related'
 
802
    _description = 'Contracts - related documents'
 
803
 
 
804
    def _get_reference_model(self, cr, uid, context=None):
 
805
        ref_models = [
 
806
            ('sale.order', 'Sales Order'), 
 
807
            ('purchase.order', 'Purchase Order'),
 
808
        ]
 
809
        return ref_models
 
810
 
 
811
    _columns = {
 
812
        'name': fields.char('Description', size=128, required=True),
 
813
        'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of contract lines."),
 
814
        'contract_id': fields.many2one('contract.document', 'Contract Reference', ondelete='cascade'),
 
815
        'document': fields.reference('Related Document', selection=_get_reference_model, required=True, size=128),
 
816
        'note': fields.text('Notes'),
 
817
    }
 
818
 
 
819
contract_document_related()
 
820
 
 
821
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
 
 
b'\\ No newline at end of file'