1
# -*- encoding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
6
# Copyright (c) 2013 Noviat nv/sa (www.noviat.be). All rights reserved.
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.
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.
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/>.
21
##############################################################################
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
30
_logger = logging.getLogger(__name__)
32
class contract_category(osv.osv):
33
_name = 'contract.category'
34
_description = 'Contract Category'
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'),
43
'company_id': lambda s, cr, uid, ctx: s.pool.get('res.company')._company_default_get(cr, uid, 'account.account', context=ctx),
47
('code', 'unique (code)', 'The code must be unique !')
51
class contract_document(osv.osv):
52
_name = 'contract.document'
53
_description = 'Contracts'
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')
64
def _get_contract_type(self, cr, uid, context=None):
65
return [('sale', 'Sale'), ('purchase', 'Purchase')]
67
def _get_company(self, cr, uid, context=None):
70
user = self.pool.get('res.users').browse(cr, uid, uid, context)
71
company_id = context.get('company_id', user.company_id.id)
74
def _get_journal(self, cr, uid, context=None):
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
83
def _get_currency(self, cr, uid, context=None):
85
journal_id = self._get_journal(cr, uid, context=context)
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
91
def _get_invoices(self, cr, uid, ids, name, args, context=None):
93
for contract in self.browse(cr, uid, ids, context=context):
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
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([
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)]}),
159
'name': _get_contract_ref,
160
'user_id': lambda s, cr, uid, ctx: uid,
163
'company_id': _get_company,
164
'journal_id': _get_journal,
165
'currency_id': _get_currency,
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
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
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
188
return {'value': result}
190
def action_confirm(self, cr, uid, ids, context):
191
return self.write(cr, uid, ids, {'state':'active'}, context=context)
193
def action_end(self, cr, uid, ids, context):
194
return self.write(cr, uid, ids, {'state':'end'}, context=context)
196
def action_cancel(self, cr, uid, ids, context):
197
return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
199
def action_draft(self, cr, uid, ids, context):
200
return self.write(cr, uid, ids, {'state':'draft'}, context=context)
202
def _get_invoice_type(self, cr, uid, contract_type, context=None):
203
if contract_type == 'sale':
205
elif contract_type == 'purchase':
208
raise NotImplementedError("Contract Type %s is not supported" % contract_type)
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')
219
for contract in self.browse(cr, uid, ids, context=context):
220
if contract.state != 'active':
223
lang = contract.partner_id.lang
225
return translate(cr, 'contract.py', 'code', lang, src) or src
228
for cl in contract.contract_line:
229
if cl.billing_result == 'none':
231
if cl.type == 'heading':
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)
239
billed = filter(lambda x: x['billed'], matches)
242
raise osv.except_osv(_('Error !'), _('Ambiguous billing table !'))
244
if invoices.get(entry['date']):
245
invoices[entry['date']] += [{'contract_line': cl, 'billing_id': matches[0].id, 'service_period': entry.get('service_period')}]
247
invoices[entry['date']] = [{'contract_line': cl, 'billing_id': matches[0].id, 'service_period': entry.get('service_period')}]
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')}]
254
invoices[entry['date']] = [{'contract_line': cl, 'billing_id': clb_id, 'service_period': entry.get('service_period')}]
256
#_logger.warn('create_invoice, invoices=%s', invoices)
258
for k,v in invoices.iteritems():
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.
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']
271
#_logger.warn('create_invoice, inv_vals=%s', inv_vals)
273
'name': contract.categ_id and ', '.join([contract.name, contract.categ_id.code]) or contract.name,
274
'origin': contract.name,
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,
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)
289
#_logger.warn('entry = %s', entry)
290
cl = entry['contract_line']
291
billing_id = entry['billing_id']
294
'section': cl.parent_id,
295
'service_period': entry.get('service_period'),
297
inv_lines.append(inv_line)
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)
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'
307
# step 2 : order lines by 1) section and 2) service period
308
inv_lines.sort(key=lambda k: (k['section'],k['service_period']))
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)
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
331
'service_period_section': service_period,
334
cl = inv_line['section']
335
heading_line_vals = {
336
'invoice_id': inv_id,
337
'contract_line_id': cl.id,
339
'sequence': cl.sequence,
341
'service_period_section': service_period,
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
348
# create invoice lines
349
cl = inv_line['contract_line']
351
'invoice_id': inv_id,
353
'service_period': inv_line['service_period'],
354
'account_id': cl.account_id.id,
355
'price_unit': cl.price_unit,
356
'quantity': cl.quantity,
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,
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
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)
374
inv_line_print_vals = {
375
'invoice_id': inv_id,
376
'invoice_line_id': inv_line_id,
377
'contract_line_id': cl.id,
379
'sequence': cl.sequence,
381
'parent_id': heading_line_id,
382
'service_period_section': entry.get('service_period'),
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)
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)
398
class contract_line(osv.osv):
399
_name = 'contract.line'
400
_description = 'Contract Lines'
401
_order = "contract_id, sequence asc"
403
def _get_contract_line(self, cr, uid, ids, context=None):
405
for cl in self.browse(cr, uid, ids):
406
if cl.type == 'normal':
408
def _parent_ids(rec):
411
res += _parent_ids(rec.parent_id)
414
result += _parent_ids(cl.parent_id)
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)
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):
424
if line.type == 'normal':
427
amt_lines = line.child_ids
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']
437
cur = line.contract_id.currency_id
438
res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
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()
456
def _billing_end(self, cr, uid, ids, field_name, arg, context):
458
for line in self.browse(cr, uid, ids):
459
if line.billing_type == 'one_time':
460
res[line.id] = line.billing_start
462
if line.billing_unlimited:
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
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'),
483
'contract.line': (_get_contract_line, ['price_unit','tax_ids','quantity','discount'], 20),
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'),
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.'),
498
'billing_type': fields.selection([
499
('recurring','Recurring Charge'),
500
('one_time','One Time Charge'),
502
'billing_result': fields.selection([
503
('open','Open Invoice'),
504
('draft','Draft Invoice'),
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'),
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),
530
'billing_type': 'recurring',
531
'billing_result': 'draft',
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:
541
(_check_billing_period_nbr, '\nThe number of periods must be greater than zero !', ['billing_period_nbr'])
544
def onchange_type(self, cr, uid, ids, type):
545
if type == 'heading':
546
return {'value':{'billing_type': None}}
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}}
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}}
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)
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')
576
company_id = company_id if company_id != None else context.get('company_id',False)
577
context.update({'company_id': company_id})
579
raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !"))
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
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
591
multi_currency = False
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
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
611
account_id = product.categ_id.property_account_income_categ.id
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
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
629
account_id = product.property_account_expense_categ.id
632
price_unit = price_unit * currency.rate
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,
643
'account_id': account_id,
645
if contract_type == 'sale':
646
value['billing_result'] = billing_result
647
#_logger.warn('onchange_product_id, value=%s', value)
648
return {'value': value}
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):
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)
658
taxes = account.tax_ids
659
unique_tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
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}}
671
def calc_billing_table(self, cr, uid, contract_line_ids, context=None):
672
clb_obj = self.pool.get('contract.line.billing')
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()
679
#_logger.warn('calc_billing_table, billing_start=%s, today=%s', billing_start, today)
681
if cline.billing_type == 'one_time':
684
'date': billing_start.isoformat(),
686
clb_tables.append(clb_table)
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
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
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
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
716
number = int(diff_year/multiplier + x)
718
for i in range(number):
719
if base_period == 'day':
720
billing_date = billing_start + timedelta(i)
722
service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + timedelta(multiplier - 1)).isoformat())
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)
728
service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + timedelta(7*multiplier - 1)).isoformat())
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)
734
service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + relativedelta(months=multiplier, days= -1)).isoformat())
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)
740
service_period = '%s - %s' %(billing_date.isoformat(), (billing_date + relativedelta(years=multiplier, days= -1)).isoformat())
742
service_period = '%s - %s' %((billing_date - relativedelta(years=multiplier)).isoformat(), (billing_date - timedelta(1)).isoformat())
744
entry = clb_table.copy()
747
'date': billing_date.isoformat(),
748
'service_period': service_period,
750
#_logger.warn('clb_table=%s', entry)
751
clb_tables += [entry]
753
#_logger.warn('calc_billing_table, return clb_tables=%s', clb_tables)
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()
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
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)
779
class contract_line_billing(osv.osv):
780
_name = 'contract.line.billing'
781
_description = 'Contract Line Billing History'
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'),
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}}
798
contract_line_billing()
800
class contract_document_related(osv.osv):
801
_name = 'contract.document.related'
802
_description = 'Contracts - related documents'
804
def _get_reference_model(self, cr, uid, context=None):
806
('sale.order', 'Sales Order'),
807
('purchase.order', 'Purchase Order'),
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'),
819
contract_document_related()
821
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
b'\\ No newline at end of file'