Commit 5dcbaa61 authored by Luca Cristaldi's avatar Luca Cristaldi

Add honorary memebrship fee

parent e0c5c8a4
......@@ -22,3 +22,11 @@ class SequenceMissingError(ValidationError):
class MemberUnpaidFeeError(ValidationError):
pass
class AmountLessThanZeroError(ValidationError):
pass
class PartyAccountPayableRequiredError(ValidationError):
pass
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from trytond.config import config
from trytond.i18n import gettext
......@@ -11,7 +10,8 @@ from trytond.model.exceptions import AccessError
from trytond.transaction import Transaction
from trytond.wizard import StateTransition, Wizard, StateView, Button
from .exceptions import PeriodDateOverlapError
from .exceptions import (PeriodDateOverlapError, AmountLessThanZeroError,
PartyAccountPayableRequiredError)
__all__ = [
'Membership', 'Period', 'Fee', 'Line', 'PostFee', 'GenerateFee',
......@@ -25,7 +25,10 @@ _STATES = {'readonly': Eval('state') != 'open'}
_LINE_STATES = {'readonly': Eval('member_state', '') != 'draft'}
_FEE_STATES = {'readonly': Bool(Eval('move'))}
_FEE_STATES = {
'readonly':
Bool(Eval('move')) and Eval('fee', Decimal('0.0')) == Decimal('0.0')
}
_MEMBER_STATES = [
('draft', 'Draft'),
......@@ -167,6 +170,13 @@ class Period(ModelSQL, ModelView):
super(Period, cls).validate(periods)
for period in periods:
period.check_dates()
period.check_fee()
def check_fee(self):
if self.amount < Decimal('0.0'):
raise AmountLessThanZeroError(
gettext('association.msg_period_fee_invalid',
period=self.rec_name))
def check_dates(self):
transaction = Transaction()
......@@ -221,6 +231,13 @@ class Fee(ModelSQL, ModelView):
"Move State"), 'on_change_with_move_state')
paid = fields.Function(fields.Boolean('paid'), 'is_paid')
reconciled = fields.Function(fields.Date('Reconciled'), 'get_reconciled')
amount = fields.Function(fields.Numeric("Amount", fee_digit),
'on_change_with_amount')
@fields.depends('period', '_parent_period.amount')
def on_change_with_amount(self, name=None):
if self.period:
return self.period.amount
@classmethod
def __setup__(cls):
......@@ -234,8 +251,10 @@ class Fee(ModelSQL, ModelView):
cls._buttons.update({
'post_move': {
'invisible': Eval('move_state') == 'posted',
'depends': ['move_state'],
'invisible':
(Eval('move_state') == 'posted')
| (Eval('amount', Decimal('0.0')) == Decimal('0.0')),
'depends': ['move_state', 'amount'],
},
})
......@@ -247,7 +266,7 @@ class Fee(ModelSQL, ModelView):
@classmethod
def delete(cls, fees):
for fee in fees:
if fee.move:
if fee.move or fee.paid:
raise AccessError(
gettext('association.msg_fee_delete', fee=fee.rec_name))
super(Fee, cls).delete(fees)
......@@ -281,13 +300,19 @@ class Fee(ModelSQL, ModelView):
return max(r.date for r in reconciliations)
def is_paid(self, name):
return self.reconciled is not None
return (self.reconciled is not None) or (
self.move is None and self.period.amount == Decimal('0.0'))
def get_move_line(self, amount, amount_second_currency):
'Return counterpart Move Line for the amount'
pool = Pool()
MoveLine = pool.get('account.move.line')
if not self.member.party.account_receivable:
raise PartyAccountPayableRequiredError(
gettext('association.msg_party_receivable_missing',
party=self.member.party.rec_name))
account = None
if amount > 0:
account = self.member.party.account_receivable
......@@ -334,12 +359,13 @@ class Fee(ModelSQL, ModelView):
pool = Pool()
Date = pool.get('ir.date')
Move = pool.get("account.move")
to_post = []
for fee in fees:
if not fee.move:
if not fee.move and fee.period.amount > Decimal('0.0'):
fee.move = fee.create_move(Date.today())
fee.save()
to_post.append(fee.move)
to_post = [x.move for x in fees]
Move.post(to_post)
@classmethod
......
......@@ -15,5 +15,11 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_unpaid_fee">
<field name="text">You cannot expel the member "%(member)s" because there are still some unpaid fees</field>
</record>
<record model="ir.message" id="msg_period_fee_invalid">
<field name="text">The period "%(period)s" fee amount should have a value >= zero</field>
</record>
<record model="ir.message" id="msg_party_receivable_missing">
<field name="text">The party "%(party)s" doesn't have an account receivable defined</field>
</record>
</data>
</tryton>
\ No newline at end of file
===============
Member Scenario
===============
Imports::
>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from decimal import Decimal
>>> from operator import attrgetter
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
>>> from trytond.modules.account.tests.tools import create_fiscalyear, \
... create_chart, get_accounts, create_tax, create_tax_code
>>> from trytond.modules.association.tests.tools import create_period
>>> from decimal import *
>>> today = datetime.date.today()
Install association::
>>> config = activate_modules('association')
Create company::
>>> _ = create_company()
>>> company = get_company()
Create fiscal year::
>>> fiscalyear = create_fiscalyear(company)
>>> fiscalyear.click('create_period')
>>> period = fiscalyear.periods[0]
Create chart of accounts::
>>> _ = create_chart(company)
>>> accounts = get_accounts(company)
>>> receivable = accounts['receivable']
>>> payable = accounts['payable']
>>> revenue = accounts['revenue']
>>> expense = accounts['expense']
>>> account_tax = accounts['tax']
>>> account_cash = accounts['cash']
Create party::
>>> Party = Model.get('party.party')
>>> party = Party(name='Party')
>>> party.account_payable = payable
>>> party.account_receivable = receivable
>>> party.save()
Create member::
>>> Member = Model.get('association.member')
>>> member = Member()
>>> member.party = party
>>> member.save()
Get a revenue journal::
>>> Journal = Model.get('account.journal')
>>> journal_revenue, = Journal.find([
... ('code', '=', 'REV'),
... ])
Create honorary membership::
>>> Membership = Model.get('association.membership')
>>> Period = Model.get('association.membership.period')
>>> honorary = Membership()
>>> honorary.party = party
>>> honorary.name = "testo"
>>> honorary.account_revenue = revenue
>>> honorary.journal = journal_revenue
>>> start_date = datetime.date(2018,1,1)
>>> timedelta = datetime.timedelta(weeks=4)
>>> fiscalMonth = 13
>>> datedeltas = [(start_date + timedelta*(n-1) + datetime.timedelta(days=1),start_date + timedelta*n,f"period {n}") for n in range(1,fiscalMonth+1)]
>>> _ = [ honorary.periods.new(start_date=period[0],end_date=period[1], name=period[2], amount=Decimal('0.0')) for period in datedeltas ]
>>> honorary.save()
Add membership to member::
>>> _ = member.memberships.new(membership=honorary)
>>> member.save()
Enroll member::
>>> member.join_date = start_date
>>> member.click("run")
>>> member.save()
Create fee lines::
>>> create_fee = Wizard('association.membership.fee_create')
>>> create_fee.form.date = start_date + datetime.timedelta(weeks=40)
>>> create_fee.execute('create_')
Check fees::
>>> Fee = Model.get('association.membership.fee')
>>> honorary_fees = Fee.find([('period.amount','=', Decimal('0.0'))])
>>> all([x.paid for x in honorary_fees]) and len(honorary_fees) > 0
True
......@@ -79,12 +79,17 @@ Create membership::
>>> datedeltas = [(start_date + timedelta*(n-1) + datetime.timedelta(days=1),start_date + timedelta*n,f"period {n}") for n in range(1,fiscalMonth+1)]
>>> periods = [ membership.periods.new(start_date=period[0],end_date=period[1], name=period[2], amount=Decimal(42)) for period in datedeltas ]
>>> membership.save()
>>> overlapping =membership.periods.new(start_date=start_date,end_date=start_date+timedelta,name="overlapping",amount=Decimal(42)).save()
>>> overlapping =membership.periods.new(start_date=start_date,end_date=start_date+timedelta,name="overlapping",amount=Decimal(42)).save() #doctest: +ELLIPSIS
Traceback (most recent call last):
...
trytond.modules.association.exceptions.PeriodDateOverlapError: ('UserError', ('The period "overlapping" overlaps with the period "period 1"', ''))
trytond.modules.association.exceptions.PeriodDateOverlapError: ('UserError', ...)
>>> new_date = datetime.date(2020,1,1)
>>> inverted = membership.periods.new(start_date=new_date + timedelta,end_date=start_date,name="inverted",amount=Decimal(42)).save()
>>> inverted = membership.periods.new(start_date=new_date + timedelta,end_date=start_date,name="inverted",amount=Decimal(42)).save() #doctest: +ELLIPSIS
Traceback (most recent call last):
...
trytond.model.modelstorage.DomainValidationError: ('UserError', ('The value for field "Ending Date" in "Membership period" is not valid according to its domain.', ''))
trytond.model.modelstorage.DomainValidationError: ('UserError', ...)
>>> negative = membership.periods.new(start_date=new_date,end_date=new_date + timedelta,name="negative 1",amount=Decimal(-10)).save() #doctest: +ELLIPSIS
Traceback (most recent call last):
...
trytond.modules.association.exceptions.AmountLessThanZeroError: ('UserError', ...)
......@@ -100,8 +100,18 @@ Add membership to member::
>>> member.leave_date = start_date + datetime.timedelta(weeks=35)
>>> member.save()
Enroll member::
>>> member.join_date = start_date
>>> member.click("run")
>>> member.save()
Create fee lines::
>>> Fee = Model.get('association.membership.fee')
>>> create_fee = Wizard('association.membership.fee_create')
>>> create_fee.form.date = start_date + datetime.timedelta(weeks=40)
>>> create_fee.execute('create_')
>>> len(Fee.find([])) > 0
True
......@@ -36,4 +36,10 @@ def suite():
encoding='utf-8',
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE,
checker=doctest_checker))
suite.addTests(
doctest.DocFileSuite('scenario_honorary_membership.rst',
tearDown=doctest_teardown,
encoding='utf-8',
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE,
checker=doctest_checker))
return suite
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