Commit 83d9a785 authored by Luca Cristaldi's avatar Luca Cristaldi

Add print membership wizard and report

Some code refactoring
Fix member search
Move the memberships view in the Configuration tab
add documentation
parent 4e331564
......@@ -20,11 +20,15 @@ def register():
Move,
GenerateFeeStart,
Cron,
PrintMembersBookStart,
module='association', type_='model')
Pool.register(
PostFee,
GenerateFee,
MembersBookWizard,
module='association', type_='wizard')
Pool.register(module='association', type_='report')
Pool.register(
MembersBookReport,
module='association', type_='report')
Association Module
##################
* association.membership
* name
* periods
* account_revenue
* company (from CompanyMultiValueMixin)
* association.membership.period
* name
* start_date
* end_date
* amount
* membership (many2One)
* company (function)
* association.membership.fee
* member (many2one of association.member)
* amount
* period (the association.membership.period associated to this line)
* payment_lines (a function field, like the one in the invoice model)
* move (the account.move.line associate with this fee line)
* paid (store the date when the move was reconciled)
* company (from CompanyMultiValueMixin)
===========
Association
===========
------
Member
------
A member is defined by:
* code: a sequential code, you can specify the format in the Configuration tab
* Party: The party associated with this member
* join_date: The date were the member joins the association
* leave_date: The date were the member leaves the association
* memberships: A list of Mebership Line
If you want easilly check if the member paied all his fees, you can use the "Fees" relate utility.
---------------
Membership Line
---------------
The membership line is used to specify the starting and ending date of the membership.
This so you can selecet a starting/ending date different from the member one.
If you leave the fields empty, it will use the join/leave date from the member.
A membership line is defined by:
* membership: the membership the member enrolled
* start_date: the strating date, if not specified use the join date from the member will be used
* end_date: the ending date, if not specified the leave date from the member will be used
This is used by the "Create Fees" Wizard to filter wich period the user should be billed
----------
Membership
----------
A membership is a collection of Periods.
The list must contain Periods with no overlapping date.
A Membership is defined by:
* name: The name of the Membership
* journal: The Journal that will be used for posting accounting move
* account_revenue: The account revenue that will be used for posting accounting move
* periods: A list of Periods
*******************
Print Member's Book
*******************
This wizard will print a Report of all the member that are not in the draft state.
------
Period
------
A periods define a date range and what the member should pay for the period
A period is defined by:
* start_date: the starting date of the period
* end_date: the ending date of the period
* fee: the amount that the member should pay for this period
---
Fee
---
A fee is associated to a Member and a Period.
Tryton will check if a Member have Fees with the same period,and will throw an error if you try to do so.
A feee is defined as:
* member: The Member associated with this fee
* period: The Period associated with this fee
* move: The move associated with this fee
The module will check if the move is reconciled and posted, if so the paid field will be checked.
You can post the move via the Post button/action, or manually.
******************
Create Fees Wizard
******************
The wizard will generate fee record based on the date.
This wizard is used by the cronjob that will trigger (by default) once every day.
......@@ -16,12 +16,6 @@ class DateNotInPeriodError(PeriodValidationError):
class SequenceMissingError(ValidationError):
pass
class MemberUnpaiedFeeError(ValidationError):
pass
#cls._error_messages.update({
# 'no_sequence':
# ('No sequence defined for member. You must '
# 'define a sequence or enter the member\'s number.'),
# })
......@@ -3,12 +3,13 @@ from trytond.pyson import Eval, If
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.i18n import gettext
from trytond.wizard import Wizard, StateView, Button, StateReport
from trytond.report import Report
from .exceptions import MemberUnpaiedFeeError
#TODO: create code only on state running, view subscription
__all__ = ['Member']
__all__ = [
'Member', 'MembersBookWizard', 'PrintMembersBookStart', 'MembersBookReport'
]
STATES = [
('draft', 'Draft'),
......@@ -22,12 +23,13 @@ _STATE = {'readonly': Eval('state') != 'draft'}
class Member(Workflow, ModelSQL, ModelView):
"""Member"""
'Member'
__name__ = "association.member"
_rec_name = 'party_name'
party_name = fields.Function(fields.Char("Party Name"), 'on_change_with_party_name')
party_name = fields.Function(fields.Char("Party Name"),
'on_change_with_party_name',searcher='search_party_member')
state = fields.Selection(STATES, 'State', readonly=True)
code = fields.Char('Code',
required=True,
......@@ -37,13 +39,12 @@ class Member(Workflow, ModelSQL, ModelView):
help="The unique identifier of the associate.")
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
'get_code_readonly')
association = fields.Many2One(
'company.company',
'Association',
states=_STATE,
depends=_DEPENDS,
required=True,
help="The association this member belongs to")
company = fields.Many2One('company.company',
'Company',
states=_STATE,
depends=_DEPENDS,
required=True,
help="The association this member belongs to")
party = fields.Many2One('party.party',
'Party',
states=_STATE,
......@@ -86,12 +87,7 @@ class Member(Workflow, ModelSQL, ModelView):
"Memberships",
depends=['state'],
states={'readonly': Eval('state') != 'draft'})
# fees = fields.One2Many('association.membership.fee',
# 'member',
# 'fee',
# states={'readonly': Eval('state') != 'draft'},
# depends=['state'])
#TODO: check via sql verifi method
@classmethod
def __setup__(cls):
......@@ -100,8 +96,7 @@ class Member(Workflow, ModelSQL, ModelView):
cls._sql_constraints = [
('code_uniq', Unique(t, t.code), 'Member code must be unique.'),
]
cls._transitions |= {('draft', 'running'),
('running', 'draft'),
cls._transitions |= {('draft', 'running'), ('running', 'draft'),
('running', 'stopped')}
cls._buttons.update({
......@@ -119,7 +114,7 @@ class Member(Workflow, ModelSQL, ModelView):
},
})
@fields.depends('party','_parent_party.name')
@fields.depends('party', '_parent_party.name')
def on_change_with_party_name(self, name=None):
if self.party:
return self.party.name
......@@ -137,6 +132,10 @@ class Member(Workflow, ModelSQL, ModelView):
if sequence:
return Sequence.get_id(sequence.id)
@classmethod
def search_party_member(cls,name, clause):
return [('party',) + tuple(clause[1:])]
@classmethod
def create(cls, vlist):
for values in vlist:
......@@ -157,16 +156,16 @@ class Member(Workflow, ModelSQL, ModelView):
def default_state(cls):
return 'draft'
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@classmethod
def default_code_readonly(cls, **pattern):
Configuration = Pool().get('association.configuration')
config = Configuration(1)
return bool(config.get_multivalue('member_sequence', **pattern))
@classmethod
def default_association(cls):
return Transaction().context.get('company')
@classmethod
@ModelView.button
@Workflow.transition('draft')
......@@ -195,9 +194,59 @@ class Member(Workflow, ModelSQL, ModelView):
pending_fees = Fee.search([('member', 'in', members)])
if not all(x.paid for x in pending_fees):
raise MemberUnpaiedFeeError(gettext('association.msg_unpaid_fee',member=", ".join([x.party_name for x in members])))
raise MemberUnpaiedFeeError(
gettext('association.msg_unpaid_fee',
member=", ".join([x.party_name for x in members])))
for member in members:
if not member.leave_date:
member.leave_date = Date.today()
cls.save(members)
class MembersBookReport(Report):
'Members Book report'
__name__ = 'association.member_print'
@classmethod
def get_context(cls, records, data):
pool = Pool()
Member = pool.get('association.member')
Company = pool.get('company.company')
clause = [('state', '!=', 'draft'), ('company', '=', data['company'])]
context = super(MembersBookReport, cls).get_context(records, data)
members = Member.search(clause)
context["members"] = members
context["company"] = Company(data['company'])
print(context)
return context
class PrintMembersBookStart(ModelView):
'Print Members Book'
__name__ = 'association.member.print_member_book.start'
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
class MembersBookWizard(Wizard):
'Print member book'
__name__ = 'association.member.print_member_book'
start = StateView(
'association.member.print_member_book.start',
'association.print_member_book_start_view_from', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-print', default=True)
])
print_ = StateReport('association.member_print')
def do_print_(self, action):
data = {
'company': self.start.company.id,
}
return action, data
......@@ -128,5 +128,28 @@
<field name="name">Member</field>
<field name="code">association.member</field>
</record>
<!-- Report -->
<record model="ir.action.report" id="report_members_book">
<field name="name">Member Book</field>
<field name="model">association.member</field>
<field name="report_name">association.member_print</field>
<field name="report">association/memberbook.fodt</field>
</record>
<record model="ir.action.keyword" id="report_members_book_keyword">
<field name="keyword">form_print</field>
<field name="model">association.member,-1</field>
<field name="action" ref="report_members_book"/>
</record>
<!-- Wizard -->
<record model="ir.ui.view" id="print_member_book_start_view_from">
<field name="model">association.member.print_member_book.start</field>
<field name="type">form</field>
<field name="name">members_book_start_form</field>
</record>
<record model="ir.action.wizard" id="wizard_print_member_book">
<field name="name">Print Member Book</field>
<field name="wiz_name">association.member.print_member_book</field>
</record>
<menuitem parent="menu_association" sequence="80" action="wizard_print_member_book" id="menu_wizard_print_member_book" icon="tryton-print"/>
</data>
</tryton>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
......@@ -32,7 +32,7 @@
<field name="priority" eval="20" />
<field name="name">membership_period_tree_editable</field>
</record>
<menuitem name="Memberships" sequence="20" parent="menu_association" id="menu_membership" action="act_membership_form" icon="tryton-list" />
<menuitem name="Memberships" sequence="20" parent="menu_configuration" id="menu_membership" action="act_membership_form" icon="tryton-list" />
<!-- membership fee -->
<record model="ir.ui.view" id="membership_fee_view_tree">
<field name="model">association.membership.fee</field>
......@@ -55,7 +55,7 @@
<field name="view" ref="membership_fee_view_tree" />
<field name="act_window" ref="membership_fee_view_form" />
</record>
<record model="ir.action.act_window.view" id="act_fee_form2">
<record model="ir.action.act_window.view" id="act_fee_form2">
<field name="sequence">10</field>
<field name="view" ref="membership_fee_view_form" />
<field name="act_window" ref="membership_fee_view_tree" />
......@@ -87,12 +87,12 @@
<field name="model">association.member,-1</field>
<field name="action" ref="act_membership_fee_create" />
</record>
<menuitem parent="menu_association" sequence="90" action="act_membership_fee_create" id="menu_act_membership_fee_create" />
<record model="ir.ui.view" id="create_membership_fee_view_form">
<field name="model">association.membership.fee_create.start</field>
<field name="type">form</field>
<field name="name">membership_fee_wizard_form</field>
</record>
<menuitem parent="menu_association" sequence="90" action="act_membership_fee_create" id="menu_act_membership_fee_create" />
<!-- membership line -->
<record model="ir.ui.view" id="membership_line_view_tree_editable">
<field name="model">association.membership.line</field>
......@@ -129,9 +129,7 @@
<record model="ir.action.act_window" id="act_fees_form_relate">
<field name="name">Fees</field>
<field name="res_model">association.membership.fee</field>
<field name="domain"
eval="[('member.id', 'in', Eval('active_ids'))]"
pyson="1" />
<field name="domain" eval="[('member.id', 'in', Eval('active_ids'))]" pyson="1" />
</record>
<record model="ir.action.keyword" id="act_open_fees_form_relate">
<field name="keyword">form_relate</field>
......@@ -141,7 +139,7 @@
<!-- Create Fees Cronjob -->
<record model="ir.cron" id="cron_generate_invoice">
<field name="method">association.membership.fee|generate_fees</field>
<field name="interval_number" eval="1"/>
<field name="interval_number" eval="1" />
<field name="interval_type">days</field>
</record>
</data>
......
......@@ -77,6 +77,7 @@ Create member::
>>> Member = Model.get('association.member')
>>> member = Member()
>>> member.party = party
>>> member.company = company
>>> membership_line = member.memberships.new(membership=membership)
>>> member.save()
>>> membership_line.start_date = start_date
......@@ -103,12 +104,45 @@ Test membership unique::
Test memebrship fee::
>>> Period = Model.get('association.membership.period')
>>> Fee = Model.get('association.membership.fee')
>>> period, = Period.find(["name","=","period 1"])
>>> fee = member.fees.new(period=period)
>>> fee = Fee(member=member,period=period)
>>> fee.save() #doctest: +ELLIPSIS
Traceback (most recent call last):
...
trytond.model.modelstorage.DomainValidationError: ('UserError', (...))
Make member active::
>>> member.click('run')
>>> member.state
'running'
Test memebrship fee move::
>>> Period = Model.get('association.membership.period')
>>> Fee = Model.get('association.membership.fee')
>>> period, = Period.find(["name","=","period 1"])
>>> fee = Fee(member=member,period=period)
>>> fee.save()
>>> member.save()
>>> fee.click('post_move')
>>> fee.move
proteus.Model.get('account.move')(1)
>>> fee.move #doctest: +ELLIPSIS
proteus.Model.get('account.move')(...)
Test member expulsion::
>>> member.click('stop') #doctest: +ELLIPSIS
Traceback (most recent call last):
...
trytond.modules.association.exceptions.MemberUnpaiedFeeError: (...)
>>> move_wiz = Wizard('account.move.cancel',[fee.move])
>>> move_wiz.form.description = 'Cancel'
>>> move_wiz.execute('cancel')
>>> fee.move.reload()
>>> fee.paid
True
>>> member.click('stop')
>>> member.state
'stopped'
<form col="2">
<image name="tryton-info" xexpand="0" xfill="0"/>
<group col="2" xexpand="1" id="association_company">
<label string="Create the Book of members of"
id="create"/>
<field name="company"/>
</group>
</form>
\ No newline at end of file
......@@ -14,6 +14,10 @@
<page string="Memberships" id="membership_lines">
<field name="memberships" colspan="6" view_ids="association.membership_line_view_tree_editable" />
</page>
<page string="other" id="other">
<label name="company" />
<field name="company" />
</page>
<!-- <page string="Fees" id="fees_lines">
<field name="fees" colspan="6" view_ids="association.membership_fee_view_tree" />
</page> -->
......
......@@ -4,5 +4,5 @@
<label string="Create Membership fees for date"
id="create"/>
<field name="date"/>
</group>
</group>
</form>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment