1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.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 General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (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 General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from osv import fields, osv
24 from tools import config
28 ('none', 'Non Member'),
29 ('canceled', 'Canceled Member'),
30 ('old', 'Old Member'),
31 ('waiting', 'Waiting Member'),
32 ('invoiced', 'Invoiced Member'),
33 ('associated', 'Associated Member'),
34 ('free', 'Free Member'),
35 ('paid', 'Paid Member'),
49 class res_partner(osv.osv):
50 _inherit = 'res.partner'
52 'associate_member': fields.many2one('res.partner', 'Associate member'),
56 REQUETE = '''SELECT partner, state FROM (
57 SELECT members.partner AS partner,
58 CASE WHEN MAX(members.state) = 0 THEN 'none'
59 ELSE CASE WHEN MAX(members.state) = 1 THEN 'canceled'
60 ELSE CASE WHEN MAX(members.state) = 2 THEN 'old'
61 ELSE CASE WHEN MAX(members.state) = 3 THEN 'waiting'
62 ELSE CASE WHEN MAX(members.state) = 4 THEN 'invoiced'
63 ELSE CASE WHEN MAX(members.state) = 5 THEN 'associated'
64 ELSE CASE WHEN MAX(members.state) = 6 THEN 'free'
65 ELSE CASE WHEN MAX(members.state) = 7 THEN 'paid'
66 END END END END END END END END
69 CASE WHEN MAX(inv_digit.state) = 4 THEN 7
70 ELSE CASE WHEN MAX(inv_digit.state) = 3 THEN 4
71 ELSE CASE WHEN MAX(inv_digit.state) = 2 THEN 3
72 ELSE CASE WHEN MAX(inv_digit.state) = 1 THEN 1
76 SELECT p.id as partner,
77 CASE WHEN ai.state = 'paid' THEN 4
78 ELSE CASE WHEN ai.state = 'open' THEN 3
79 ELSE CASE WHEN ai.state = 'proforma' THEN 2
80 ELSE CASE WHEN ai.state = 'draft' THEN 2
81 ELSE CASE WHEN ai.state = 'cancel' THEN 1
85 JOIN account_invoice ai ON (
88 JOIN account_invoice_line ail ON (
89 ail.invoice_id = ai.id
91 JOIN membership_membership_line ml ON (
92 ml.account_invoice_line = ail.id
94 WHERE ml.date_from <= '%s'
95 AND ml.date_to >= '%s'
103 SELECT p.id AS partner,
104 CASE WHEN p.free_member THEN 6
105 ELSE CASE WHEN p.associate_member IN (
106 SELECT ai.partner_id FROM account_invoice ai JOIN
107 account_invoice_line ail ON (ail.invoice_id = ai.id AND ai.state = 'paid')
108 JOIN membership_membership_line ml ON (ml.account_invoice_line = ail.id)
109 WHERE ml.date_from <= '%s'
110 AND ml.date_to >= '%s'
117 OR p.associate_member > 0
119 SELECT p.id as partner,
120 MAX(CASE WHEN ai.state = 'paid' THEN 2
125 JOIN account_invoice ai ON (
128 JOIN account_invoice_line ail ON (
129 ail.invoice_id = ai.id
131 JOIN membership_membership_line ml ON (
132 ml.account_invoice_line = ail.id
134 WHERE ml.date_from < '%s'
135 AND ml.date_to < '%s'
136 AND ml.date_from <= ml.date_to
141 GROUP BY members.partner
148 class membership_line(osv.osv):
151 def _check_membership_date(self, cr, uid, ids, context=None):
152 '''Check if membership product is not in the past'''
155 SELECT MIN(ml.date_to - ai.date_invoice)
156 FROM membership_membership_line ml
157 JOIN account_invoice_line ail ON (
158 ml.account_invoice_line = ail.id
160 JOIN account_invoice ai ON (
161 ai.id = ail.invoice_id)
163 ''' % ','.join([str(id) for id in ids]))
167 if r[0] and r[0] < 0:
171 def _state(self, cr, uid, ids, name, args, context=None):
172 '''Compute the state lines'''
174 for line in self.browse(cr, uid, ids):
177 account_invoice i WHERE
179 SELECT l.invoice_id FROM
180 account_invoice_line l WHERE
182 SELECT ml.account_invoice_line FROM
183 membership_membership_line ml WHERE
188 fetched = cr.fetchone()
190 res[line.id] = 'canceled'
194 if (istate == 'draft') | (istate == 'proforma'):
196 elif istate == 'open':
198 elif istate == 'paid':
200 elif istate == 'cancel':
206 _description = __doc__
207 _name = 'membership.membership_line'
209 'partner': fields.many2one('res.partner', 'Partner', ondelete='cascade', select=1),
210 'date_from': fields.date('From'),
211 'date_to': fields.date('To'),
212 'date_cancel' : fields.date('Cancel date'),
213 'account_invoice_line': fields.many2one('account.invoice.line', 'Account Invoice line'),
214 'state': fields.function(_state, method=True, string='State', type='selection', selection=STATE),
216 _rec_name = 'partner'
219 (_check_membership_date, 'Error, this membership product is out of date', [])
225 class Partner(osv.osv):
228 def _membership_state_search_inv(self,cr,uid,ids):
229 data_inv = self.pool.get('account.invoice').browse(cr,uid,ids)
231 for data in data_inv:
232 list_partner.append(data.partner_id.id)
235 def _membership_state(self, cr, uid, ids, name, args, context=None):
239 today = time.strftime('%Y-%m-%d')
242 partner_data = self.browse(cr,uid,id)
243 if partner_data.membership_cancel and today > partner_data.membership_cancel:
246 if partner_data.membership_stop and today > partner_data.membership_stop:
250 if partner_data.member_lines:
251 for mline in partner_data.member_lines:
252 if mline.date_from <= today and mline.date_to >= today:
253 if mline.account_invoice_line and mline.account_invoice_line.invoice_id:
254 mstate = mline.account_invoice_line.invoice_id.state
258 elif mstate == 'open' and s!=0:
260 elif mstate == 'cancel' and s!=0 and s!=1:
262 elif (mstate == 'draft' or mstate == 'proforma') and s!=0 and s!=1:
265 for mline in partner_data.member_lines:
266 if mline.date_from < today and mline.date_to < today and mline.date_from<=mline.date_to and mline.account_invoice_line.invoice_id.state == 'paid':
282 if partner_data.free_member and s!=0:
284 if partner_data.associate_member:
285 assciate_partner = self.browse(cr,uid,partner_data.associate_member.id)
286 cr.execute('select membership_state from res_partner where id=%d', (partner_data.id,))
287 data_partner_state = cr.fetchall()
288 for i in assciate_partner.member_lines:
289 if i.date_from <= today and i.date_to >= today and i.account_invoice_line.invoice_id.state == 'paid' and s!=0 and data_partner_state[0][0] !='free':
290 res[id] = 'associated'
292 # '''Compute membership state of partners'''
293 # today = time.strftime('%Y-%m-%d')
297 # clause = 'WHERE partner IN (' + ','.join([str(id) for id in ids]) + ')'
298 # cr.execute(REQUETE % (today, today, today, today, today, today, clause))
299 # fetches = cr.fetchall()
300 # for fetch in fetches:
301 # res[fetch[0]] = fetch[1]
304 #no more need becaz of new functionality store attribut on function field
305 # def _membership_state_search(self, cr, uid, obj, name, args):
306 # '''Search on membership state'''
308 # today = time.strftime('%Y-%m-%d')
310 # for i in range(len(args)):
313 # clause += 'state '+args[i][1]+" '"+args[i][2]+"' "
314 # cr.execute(REQUETE % (today, today, today, today, today, today, clause))
315 # ids=[x[0] for x in cr.fetchall()]
317 # return [('id', 'in', ids)]
319 def _membership_start(self, cr, uid, ids, name, args, context=None):
320 '''Return the start date of membership'''
322 member_line_obj = self.pool.get('membership.membership_line')
323 for partner in self.browse(cr, uid, ids):
324 if partner.membership_state == 'associated':
325 partner_id = partner.associate_member.id
327 partner_id = partner.id
328 line_id = member_line_obj.search(cr, uid, [('partner', '=', partner_id)],
329 limit=1, order='date_from')
331 res[partner.id] = member_line_obj.read(cr, uid, line_id[0],
332 ['date_from'])['date_from']
334 res[partner.id] = False
337 def _membership_start_search(self, cr, uid, obj, name, args):
338 '''Search on membership start date'''
341 where = ' AND '.join(['date_from '+x[1]+' \''+str(x[2])+'\''
343 cr.execute('SELECT partner, MIN(date_from) \
345 SELECT partner, MIN(date_from) AS date_from \
346 FROM membership_membership_line \
353 return [('id', '=', '0')]
354 return [('id', 'in', [x[0] for x in res])]
356 def _membership_stop(self, cr, uid, ids, name, args, context=None):
357 '''Return the stop date of membership'''
359 member_line_obj = self.pool.get('membership.membership_line')
360 for partner in self.browse(cr, uid, ids):
361 cr.execute('select membership_state from res_partner where id=%d', (partner.id,))
362 data_state = cr.fetchall()
363 #if partner.membership_state == 'associated':
364 if data_state[0][0] == 'associated':
365 partner_id = partner.associate_member.id
367 partner_id = partner.id
368 line_id = member_line_obj.search(cr, uid, [('partner', '=', partner_id)],
369 limit=1, order='date_to desc')
371 res[partner.id] = member_line_obj.read(cr, uid, line_id[0],
372 ['date_to'])['date_to']
374 res[partner.id] = False
377 def _membership_stop_search(self, cr, uid, obj, name, args):
378 '''Search on membership stop date'''
381 where = ' AND '.join(['date_to '+x[1]+' \''+str(x[2])+'\''
383 cr.execute('SELECT partner, MAX(date_to) \
385 SELECT partner, MAX(date_to) AS date_to \
386 FROM membership_membership_line \
393 return [('id', '=', '0')]
394 return [('id', 'in', [x[0] for x in res])]
396 def _membership_cancel(self, cr, uid, ids, name, args, context=None):
397 '''Return the cancel date of membership'''
399 member_line_obj = self.pool.get('membership.membership_line')
400 for partner_id in ids:
401 line_id = member_line_obj.search(cr, uid, [('partner', '=', partner_id)],
402 limit=1, order='date_cancel')
404 res[partner_id] = member_line_obj.read(cr, uid, line_id[0],
405 ['date_cancel'])['date_cancel']
407 res[partner_id] = False
410 def _membership_cancel_search(self, cr, uid, obj, name, args):
411 '''Search on membership cancel date'''
414 where = ' AND '.join(['date_cancel '+x[1]+' \''+str(x[2])+'\''
416 cr.execute('SELECT partner, MIN(date_cancel) \
418 SELECT partner, MIN(date_cancel) AS date_cancel \
419 FROM membership_membership_line \
426 return [('id', '=', '0')]
427 return [('id', 'in', [x[0] for x in res])]
431 _inherit = 'res.partner'
433 'member_lines': fields.one2many('membership.membership_line', 'partner',
435 'membership_amount': fields.float('Membership amount', digites=(16, 2),
436 help='The price negociated by the partner'),
437 # 'membership_state': fields.function(_membership_state, method=True, string='Current membership state',
438 # type='selection', selection=STATE, fnct_search=_membership_state_search),
439 'membership_state': fields.function(_membership_state, method=True, string='Current membership state',
440 type='selection',selection=STATE,store={'account.invoice':(['state'],_membership_state_search_inv)}),
441 # 'associate_member': fields.many2one('res.partner', 'Associate member'),
442 'free_member': fields.boolean('Free member'),
443 'membership_start': fields.function(_membership_start, method=True,
444 string='Start membership date', type='date',
445 fnct_search=_membership_start_search),
446 'membership_stop': fields.function(_membership_stop, method=True,
447 string='Stop membership date', type='date',
448 fnct_search=_membership_stop_search),
449 'membership_cancel': fields.function(_membership_cancel, method=True,
450 string='Cancel membership date', type='date',
451 fnct_search=_membership_cancel_search),
454 'free_member': lambda *a: False,
455 'membership_cancel' : lambda *d : False,
460 class product_template(osv.osv):
461 _inherit = 'product.template'
463 'member_price':fields.float('Member Price', digits=(16, int(config['price_accuracy']))),
467 class Product(osv.osv):
469 def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False):
470 if ('product' in context) and (context['product']=='membership_product'):
471 model_data_ids_form = self.pool.get('ir.model.data').search(cr,user,[('model','=','ir.ui.view'),('name','in',['membership_products_form','membership_products_tree'])])
472 resource_id_form = self.pool.get('ir.model.data').read(cr,user,model_data_ids_form,fields=['res_id','name'])
474 for i in resource_id_form:
475 dict_model[i['name']]=i['res_id']
476 if view_type=='form':
477 view_id = dict_model['membership_products_form']
479 view_id = dict_model['membership_products_tree']
480 return super(Product,self).fields_view_get(cr, user, view_id, view_type, context, toolbar)
483 _inherit = 'product.product'
484 _description = 'product.product'
487 'membership': fields.boolean('Membership', help='Specify if this product is a membership product'),
488 'membership_date_from': fields.date('Date from'),
489 'membership_date_to': fields.date('Date to'),
490 # 'member_price':fields.float('Member Price'),
494 'membership': lambda *args: False
499 class Invoice(osv.osv):
502 _inherit = 'account.invoice'
505 def action_move_create(self, cr, uid, ids, context=None):
506 '''Create membership.membership_line if the product is for membership'''
509 member_line_obj = self.pool.get('membership.membership_line')
510 partner_obj = self.pool.get('res.partner')
511 for invoice in self.browse(cr, uid, ids):
513 # fetch already existing member lines
514 former_mlines = member_line_obj.search(cr,uid,
515 [('account_invoice_line','in',
516 [ l.id for l in invoice.invoice_line])], context)
519 member_line_obj.write(cr,uid,former_mlines, {'account_invoice_line':False}, context)
521 for line in invoice.invoice_line:
522 if line.product_id and line.product_id.membership:
523 date_from = line.product_id.membership_date_from
524 date_to = line.product_id.membership_date_to
525 if invoice.date_invoice > date_from and invoice.date_invoice < date_to:
526 date_from = invoice.date_invoice
527 member_line_obj.create(cr, uid, {
528 'partner': invoice.partner_id.id,
529 'date_from': date_from,
531 'account_invoice_line': line.id,
533 return super(Invoice, self).action_move_create(cr, uid, ids, context)
535 def action_cancel(self, cr, uid, ids, context=None):
536 '''Create a 'date_cancel' on the membership_line object'''
539 member_line_obj = self.pool.get('membership.membership_line')
540 today = time.strftime('%Y-%m-%d')
541 for invoice in self.browse(cr, uid, ids):
542 mlines = member_line_obj.search(cr,uid,
543 [('account_invoice_line','in',
544 [ l.id for l in invoice.invoice_line])], context)
545 member_line_obj.write(cr,uid,mlines, {'date_cancel':today}, context)
546 return super(Invoice, self).action_cancel(cr, uid, ids, context)
550 class ReportPartnerMemberYear(osv.osv):
551 '''Membership by Years'''
553 _name = 'report.partner_member.year'
554 _description = __doc__
558 'year': fields.char('Year', size='4', readonly=True, select=1),
559 'canceled_number': fields.integer('Canceled', readonly=True),
560 'waiting_number': fields.integer('Waiting', readonly=True),
561 'invoiced_number': fields.integer('Invoiced', readonly=True),
562 'paid_number': fields.integer('Paid', readonly=True),
563 'canceled_amount': fields.float('Canceled', digits=(16, 2), readonly=True),
564 'waiting_amount': fields.float('Waiting', digits=(16, 2), readonly=True),
565 'invoiced_amount': fields.float('Invoiced', digits=(16, 2), readonly=True),
566 'paid_amount': fields.float('Paid', digits=(16, 2), readonly=True),
567 'currency': fields.many2one('res.currency', 'Currency', readonly=True,
572 '''Create the view'''
574 CREATE OR REPLACE VIEW report_partner_member_year AS (
577 COUNT(ncanceled) as canceled_number,
578 COUNT(npaid) as paid_number,
579 COUNT(ninvoiced) as invoiced_number,
580 COUNT(nwaiting) as waiting_number,
581 SUM(acanceled) as canceled_amount,
582 SUM(apaid) as paid_amount,
583 SUM(ainvoiced) as invoiced_amount,
584 SUM(awaiting) as waiting_amount,
588 CASE WHEN ai.state = 'cancel' THEN ml.id END AS ncanceled,
589 CASE WHEN ai.state = 'paid' THEN ml.id END AS npaid,
590 CASE WHEN ai.state = 'open' THEN ml.id END AS ninvoiced,
591 CASE WHEN (ai.state = 'draft' OR ai.state = 'proforma')
592 THEN ml.id END AS nwaiting,
593 CASE WHEN ai.state = 'cancel'
594 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
595 ELSE 0 END AS acanceled,
596 CASE WHEN ai.state = 'paid'
597 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
599 CASE WHEN ai.state = 'open'
600 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
601 ELSE 0 END AS ainvoiced,
602 CASE WHEN (ai.state = 'draft' OR ai.state = 'proforma')
603 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
604 ELSE 0 END AS awaiting,
605 TO_CHAR(ml.date_from, 'YYYY') AS year,
606 ai.currency_id AS currency,
608 FROM membership_membership_line ml
609 JOIN (account_invoice_line ail
610 LEFT JOIN account_invoice ai
611 ON (ail.invoice_id = ai.id))
612 ON (ml.account_invoice_line = ail.id)
614 ON (ml.partner = p.id)
615 GROUP BY TO_CHAR(ml.date_from, 'YYYY'), ai.state,
616 ai.currency_id, ml.id) AS foo
617 GROUP BY year, currency)
620 ReportPartnerMemberYear()
623 class ReportPartnerMemberYearNew(osv.osv):
624 '''New Membership by Years'''
626 _name = 'report.partner_member.year_new'
627 _description = __doc__
632 'year': fields.char('Year', size='4', readonly=True, select=1),
633 'canceled_number': fields.integer('Canceled', readonly=True),
634 'waiting_number': fields.integer('Waiting', readonly=True),
635 'invoiced_number': fields.integer('Invoiced', readonly=True),
636 'paid_number': fields.integer('Paid', readonly=True),
637 'canceled_amount': fields.float('Canceled', digits=(16, 2), readonly=True),
638 'waiting_amount': fields.float('Waiting', digits=(16, 2), readonly=True),
639 'invoiced_amount': fields.float('Invoiced', digits=(16, 2), readonly=True),
640 'paid_amount': fields.float('Paid', digits=(16, 2), readonly=True),
641 'currency': fields.many2one('res.currency', 'Currency', readonly=True,
646 '''Create the view'''
648 CREATE OR REPLACE VIEW report_partner_member_year AS (
651 COUNT(ncanceled) as canceled_number,
652 COUNT(npaid) as paid_number,
653 COUNT(ninvoiced) as invoiced_number,
654 COUNT(nwaiting) as waiting_number,
655 SUM(acanceled) as canceled_amount,
656 SUM(apaid) as paid_amount,
657 SUM(ainvoiced) as invoiced_amount,
658 SUM(awaiting) as waiting_amount,
662 CASE WHEN ai.state = 'cancel' THEN ml.id END AS ncanceled,
663 CASE WHEN ai.state = 'paid' THEN ml.id END AS npaid,
664 CASE WHEN ai.state = 'open' THEN ml.id END AS ninvoiced,
665 CASE WHEN (ai.state = 'draft' OR ai.state = 'proforma')
666 THEN ml.id END AS nwaiting,
667 CASE WHEN ai.state = 'cancel'
668 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
669 ELSE 0 END AS acanceled,
670 CASE WHEN ai.state = 'paid'
671 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
673 CASE WHEN ai.state = 'open'
674 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
675 ELSE 0 END AS ainvoiced,
676 CASE WHEN (ai.state = 'draft' OR ai.state = 'proforma')
677 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
678 ELSE 0 END AS awaiting,
679 TO_CHAR(ml.date_from, 'YYYY') AS year,
680 ai.currency_id AS currency,
682 FROM membership_membership_line ml
683 JOIN (account_invoice_line ail
684 LEFT JOIN account_invoice ai
685 ON (ail.invoice_id = ai.id))
686 ON (ml.account_invoice_line = ail.id)
688 ON (ml.partner = p.id)
689 GROUP BY TO_CHAR(ml.date_from, 'YYYY'), ai.state,
690 ai.currency_id, ml.id) AS foo
691 GROUP BY year, currency)
694 ReportPartnerMemberYear()
697 class ReportPartnerMemberYearNew(osv.osv):
698 '''New Membership by Years'''
700 _name = 'report.partner_member.year_new'
701 _description = __doc__
705 'year': fields.char('Year', size='4', readonly=True, select=1),
706 'canceled_number': fields.integer('Canceled', readonly=True),
707 'waiting_number': fields.integer('Waiting', readonly=True),
708 'invoiced_number': fields.integer('Invoiced', readonly=True),
709 'paid_number': fields.integer('Paid', readonly=True),
710 'canceled_amount': fields.float('Canceled', digits=(16, 2), readonly=True),
711 'waiting_amount': fields.float('Waiting', digits=(16, 2), readonly=True),
712 'invoiced_amount': fields.float('Invoiced', digits=(16, 2), readonly=True),
713 'paid_amount': fields.float('Paid', digits=(16, 2), readonly=True),
714 'currency': fields.many2one('res.currency', 'Currency', readonly=True,
718 def init(self, cursor):
719 '''Create the view'''
721 CREATE OR REPLACE VIEW report_partner_member_year_new AS (
724 COUNT(ncanceled) AS canceled_number,
725 COUNT(npaid) AS paid_number,
726 COUNT(ninvoiced) AS invoiced_number,
727 COUNT(nwaiting) AS waiting_number,
728 SUM(acanceled) AS canceled_amount,
729 SUM(apaid) AS paid_amount,
730 SUM(ainvoiced) AS invoiced_amount,
731 SUM(awaiting) AS waiting_amount,
735 CASE WHEN ai.state = 'cancel' THEN ml2.id END AS ncanceled,
736 CASE WHEN ai.state = 'paid' THEN ml2.id END AS npaid,
737 CASE WHEN ai.state = 'open' THEN ml2.id END AS ninvoiced,
738 CASE WHEN (ai.state = 'draft' OR ai.state = 'proforma')
739 THEN ml2.id END AS nwaiting,
740 CASE WHEN ai.state = 'cancel'
741 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
742 ELSE 0 END AS acanceled,
743 CASE WHEN ai.state = 'paid'
744 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
746 CASE WHEN ai.state = 'open'
747 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
748 ELSE 0 END AS ainvoiced,
749 CASE WHEN (ai.state = 'draft' OR ai.state = 'proforma')
750 THEN SUM(ail.price_unit * ail.quantity * (1 - ail.discount / 100))
751 ELSE 0 END AS awaiting,
752 TO_CHAR(ml2.date_from, 'YYYY') AS year,
753 ai.currency_id AS currency,
757 MIN(date_from) AS date_from
758 FROM membership_membership_line
761 JOIN membership_membership_line ml2
762 JOIN (account_invoice_line ail
763 LEFT JOIN account_invoice ai
764 ON (ail.invoice_id = ai.id))
765 ON (ml2.account_invoice_line = ail.id)
766 ON (ml1.id = ml2.partner AND ml1.date_from = ml2.date_from)
768 ON (ml2.partner = p.id)
769 GROUP BY TO_CHAR(ml2.date_from, 'YYYY'), ai.state,
770 ai.currency_id, ml2.id) AS foo
771 GROUP BY year, currency
775 ReportPartnerMemberYearNew()
776 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: