Initial commit of account_dunning module
review939002
This commit is contained in:
commit
ed204a8e2e
29 files changed
+2110
No files matched your search
+306
@@ -0,0 +1,306 @@
|
||||
#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 collections import defaultdict
|
||||
|
||||
from trytond.model import Model, ModelView, ModelSQL, fields
|
||||
from trytond.pyson import If, Eval
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.wizard import Wizard, StateView, StateAction, StateTransition, \
|
||||
Button
|
||||
from trytond.pool import Pool
|
||||
|
||||
__all__ = ['Procedure', 'Level', 'Dunning',
|
||||
'CreateDunningStart', 'CreateDunning',
|
||||
'ProcessDunningStart', 'ProcessDunning']
|
||||
|
||||
|
||||
class Procedure(ModelSQL, ModelView):
|
||||
'Account Dunning Procedure'
|
||||
__name__ = 'account.dunning.procedure'
|
||||
name = fields.Char('Name', required=True, translate=True)
|
||||
levels = fields.One2Many('account.dunning.level', 'procedure', 'Levels')
|
||||
|
||||
|
||||
class Level(ModelSQL, ModelView):
|
||||
'Account Dunning Level'
|
||||
__name__ = 'account.dunning.level'
|
||||
_rec_name = 'procedure'
|
||||
sequence = fields.Integer('Sequence')
|
||||
procedure = fields.Many2One('account.dunning.procedure', 'Procedure',
|
||||
required=True, select=True)
|
||||
days = fields.Integer('Number of Overdue Days')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(Level, cls).__setup__()
|
||||
cls._order.insert(0, ('sequence', 'ASC'))
|
||||
|
||||
@staticmethod
|
||||
def order_sequence(tables):
|
||||
table, _ = tables[None]
|
||||
return [table.sequence == None, table.sequence]
|
||||
|
||||
def get_rec_name(self, name):
|
||||
return '%s@%s' % (self.procedure.levels.index(self),
|
||||
self.procedure.rec_name)
|
||||
|
||||
def test(self, line, date):
|
||||
if self.days is not None:
|
||||
return (date - line.maturity_date).days >= self.days
|
||||
|
||||
_STATES = {
|
||||
'readonly': Eval('state') != 'draft',
|
||||
}
|
||||
_DEPENDS = ['state']
|
||||
|
||||
|
||||
class Dunning(ModelSQL, ModelView):
|
||||
'Account Dunning'
|
||||
__name__ = 'account.dunning'
|
||||
company = fields.Many2One('company.company', 'Company', required=True,
|
||||
select=True, domain=[
|
||||
('id', If(Eval('context', {}).contains('company'), '=', '!='),
|
||||
Eval('context', {}).get('company', -1)),
|
||||
],
|
||||
states=_STATES, depends=_DEPENDS)
|
||||
line = fields.Many2One('account.move.line', 'Line',
|
||||
domain=[
|
||||
('account.kind', '=', 'receivable'),
|
||||
('account.company', '=', Eval('company', -1)),
|
||||
['OR',
|
||||
('debit', '>', 0),
|
||||
('credit', '<', 0),
|
||||
],
|
||||
],
|
||||
states=_STATES, depends=_DEPENDS + ['company'])
|
||||
procedure = fields.Many2One('account.dunning.procedure', 'Procedure',
|
||||
required=True, states=_STATES, depends=_DEPENDS)
|
||||
level = fields.Many2One('account.dunning.level', 'Level', required=True,
|
||||
domain=[
|
||||
('procedure', '=', Eval('procedure', -1)),
|
||||
],
|
||||
states=_STATES, depends=_DEPENDS + ['procedure'])
|
||||
blocked = fields.Boolean('Blocked')
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('done', 'Done'),
|
||||
], 'State', readonly=True)
|
||||
active = fields.Function(fields.Boolean('Active'), 'get_active',
|
||||
searcher='search_active')
|
||||
party = fields.Function(fields.Many2One('party.party', 'Party'),
|
||||
'get_line_field', searcher='search_line_field')
|
||||
amount = fields.Function(fields.Numeric('Amount',
|
||||
digits=(16, Eval('currency_digits', 2)),
|
||||
depends=['currency_digits']),
|
||||
'get_amount')
|
||||
currency_digits = fields.Function(fields.Integer('Currency Digits'),
|
||||
'get_line_field')
|
||||
maturity_date = fields.Function(fields.Date('Maturity Date'),
|
||||
'get_line_field', searcher='search_line_field')
|
||||
amount_second_currency = fields.Function(fields.Numeric(
|
||||
'Amount Second Currency',
|
||||
digits=(16, Eval('second_currency_digits', 2)),
|
||||
depends=['second_currency_digits']), 'get_amount_second_currency')
|
||||
second_currency = fields.Function(fields.Many2One('currency.currency',
|
||||
'Second Currency'), 'get_second_currency')
|
||||
second_currency_digits = fields.Function(fields.Integer(
|
||||
'Second Currency Digits'), 'get_second_currency_digits')
|
||||
|
||||
@staticmethod
|
||||
def default_company():
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@staticmethod
|
||||
def default_state():
|
||||
return 'draft'
|
||||
|
||||
def get_active(self, name):
|
||||
return not self.line.reconciliation
|
||||
|
||||
def get_line_field(self, name):
|
||||
value = getattr(self.line, name)
|
||||
if isinstance(value, Model):
|
||||
return value.id
|
||||
else:
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def search_line_field(cls, name, clause):
|
||||
return [('line.' + name,) + tuple(clause[1:])]
|
||||
|
||||
def get_amount(self, name):
|
||||
if self.line:
|
||||
return self.line.debit - self.line.credit
|
||||
|
||||
def get_amount_second_currency(self, name):
|
||||
if self.line:
|
||||
amount = self.line.debit - self.line.credit
|
||||
if self.line.amount_second_currency:
|
||||
return self.line.amount_second_currency.copy_sign(amount)
|
||||
else:
|
||||
return amount
|
||||
|
||||
def get_second_currency(self, name):
|
||||
if self.line:
|
||||
if self.line.second_currency:
|
||||
return self.line.second_currency.id
|
||||
else:
|
||||
return self.line.account.company.currency.id
|
||||
|
||||
def get_second_currency_digits(self, name):
|
||||
if self.line:
|
||||
if self.line.second_currency:
|
||||
return self.line.second_currency.digits
|
||||
else:
|
||||
return self.line.account.company.currency.digits
|
||||
|
||||
@classmethod
|
||||
def search_active(cls, name, clause):
|
||||
reverse = {
|
||||
'=': '!=',
|
||||
'!=': '=',
|
||||
}
|
||||
if clause[1] in reverse:
|
||||
if clause[2]:
|
||||
return [('line.reconciliation', clause[1], None)]
|
||||
else:
|
||||
return [('line.reconciliation', reverse[clause[1]], None)]
|
||||
else:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _overdue_line_domain(cls, date):
|
||||
return [
|
||||
('account.kind', '=', 'receivable'),
|
||||
('dunnings', '=', None),
|
||||
('maturity_date', '<=', date),
|
||||
['OR',
|
||||
('debit', '>', 0),
|
||||
('credit', '<', 0),
|
||||
],
|
||||
('party', '!=', None),
|
||||
('reconciliation', '=', None),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def generate_dunnings(cls, date=None):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
MoveLine = pool.get('account.move.line')
|
||||
|
||||
if date is None:
|
||||
date = Date.today()
|
||||
|
||||
set_level = defaultdict(list)
|
||||
for dunning in cls.search([
|
||||
('state', '=', 'done'),
|
||||
('blocked', '=', False),
|
||||
]):
|
||||
procedure = dunning.procedure
|
||||
levels = procedure.levels
|
||||
levels = levels[levels.index(dunning.level) + 1:]
|
||||
for level in levels:
|
||||
if level.test(dunning.line, date):
|
||||
break
|
||||
else:
|
||||
level = dunning.level
|
||||
if level != dunning.level:
|
||||
set_level[level].append(dunning)
|
||||
for level, dunnings in set_level.iteritems():
|
||||
cls.write(dunnings, {
|
||||
'level': level.id,
|
||||
'state': 'draft',
|
||||
})
|
||||
|
||||
lines = MoveLine.search(cls._overdue_line_domain(date))
|
||||
dunnings = (cls._get_dunning(line, date) for line in lines)
|
||||
cls.create([d._save_values for d in dunnings if d])
|
||||
|
||||
@classmethod
|
||||
def _get_dunning(cls, line, date):
|
||||
procedure = line.party.dunning_procedure
|
||||
if not procedure:
|
||||
return
|
||||
for level in procedure.levels:
|
||||
if level.test(line, date):
|
||||
break
|
||||
else:
|
||||
return
|
||||
return cls(
|
||||
line=line,
|
||||
procedure=procedure,
|
||||
level=level,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def process(cls, dunnings):
|
||||
cls.write([d for d in dunnings if not d.blocked], {
|
||||
'state': 'done',
|
||||
})
|
||||
|
||||
|
||||
class CreateDunningStart(ModelView):
|
||||
'Create Account Dunning'
|
||||
__name__ = 'account.dunning.create.start'
|
||||
date = fields.Date('Date', required=True)
|
||||
|
||||
@staticmethod
|
||||
def default_date():
|
||||
Date = Pool().get('ir.date')
|
||||
return Date.today()
|
||||
|
||||
|
||||
class CreateDunning(Wizard):
|
||||
'Create Account Dunning'
|
||||
__name__ = 'account.dunning.create'
|
||||
start = StateView('account.dunning.create.start',
|
||||
'account_dunning.dunning_create_start_view_form', [
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('Create', 'create_', 'tryton-ok', default=True),
|
||||
])
|
||||
create_ = StateAction('account_dunning.act_dunning_form')
|
||||
|
||||
def do_create_(self, action):
|
||||
pool = Pool()
|
||||
Dunning = pool.get('account.dunning')
|
||||
Dunning.generate_dunnings(date=self.start.date)
|
||||
return action, {}
|
||||
|
||||
|
||||
class ProcessDunningStart(ModelView):
|
||||
'Create Account Dunning'
|
||||
__name__ = 'account.dunning.process.start'
|
||||
|
||||
|
||||
class ProcessDunning(Wizard):
|
||||
'Process Account Dunning'
|
||||
__name__ = 'account.dunning.process'
|
||||
start = StateView('account.dunning.process.start',
|
||||
'account_dunning.dunning_process_start_view_form', [
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('process', 'process', 'tryton-ok', default=True),
|
||||
])
|
||||
process = StateTransition()
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(ProcessDunning, cls).__setup__()
|
||||
|
||||
# _actions is the list that define the order of each state to process
|
||||
# after the 'process' state.
|
||||
cls._actions = ['process']
|
||||
|
||||
def next_state(self, state):
|
||||
"Return the next state for the current state"
|
||||
try:
|
||||
i = self._actions.index(state)
|
||||
return self._actions[i + 1]
|
||||
except (ValueError, IndexError):
|
||||
return 'end'
|
||||
|
||||
def transition_process(self):
|
||||
pool = Pool()
|
||||
Dunning = pool.get('account.dunning')
|
||||
dunnings = Dunning.browse(Transaction().context['active_ids'])
|
||||
Dunning.process(dunnings)
|
||||
return self.next_state('process')
|
||||
Reference in new issue
Block a user