Files
tryton/modules/account_dunning/dunning.py
T

387 lines
12 KiB
Python
Raw Normal View History

2014-12-02 12:54:35 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2021-12-11 17:58:41 +01:00
from collections import defaultdict
from itertools import groupby
2013-09-18 22:52:41 +02:00
2023-01-19 11:25:25 +01:00
from sql import Literal
2021-12-11 17:58:41 +01:00
from trytond.model import (
Index, Model, ModelSQL, ModelView, Unique, fields, sequence_ordered)
2021-12-11 17:58:41 +01:00
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool
2021-05-10 18:40:36 +02:00
from trytond.pyson import Eval
from trytond.transaction import Transaction, check_access
2021-12-11 17:58:41 +01:00
from trytond.wizard import (
Button, StateAction, StateTransition, StateView, Wizard)
2021-08-21 09:09:55 +02:00
2013-09-18 22:52:41 +02:00
class Procedure(ModelSQL, ModelView):
'Account Dunning Procedure'
__name__ = 'account.dunning.procedure'
2018-03-09 11:50:18 +01:00
name = fields.Char('Name', required=True, translate=True,
help="The main identifier of the Dunning Procedure.")
2013-09-18 22:52:41 +02:00
levels = fields.One2Many('account.dunning.level', 'procedure', 'Levels')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('name', 'ASC'))
2013-09-18 22:52:41 +02:00
2016-09-13 14:03:15 +02:00
class Level(sequence_ordered(), ModelSQL, ModelView):
2013-09-18 22:52:41 +02:00
'Account Dunning Level'
__name__ = 'account.dunning.level'
procedure = fields.Many2One(
'account.dunning.procedure', "Procedure", required=True)
2018-03-09 11:50:18 +01:00
overdue = fields.TimeDelta('Overdue',
help="When the level should be applied.")
2016-09-02 10:56:01 +02:00
2013-09-18 22:52:41 +02:00
def get_rec_name(self, name):
return '%s@%s' % (self.procedure.levels.index(self),
self.procedure.rec_name)
def test(self, line, date):
2016-09-02 10:56:01 +02:00
if self.overdue is not None:
return (date - line.maturity_date) >= self.overdue
2013-09-18 22:52:41 +02:00
2020-03-01 12:33:51 +01:00
2013-09-18 22:52:41 +02:00
_STATES = {
'readonly': Eval('state') != 'draft',
}
class Dunning(ModelSQL, ModelView):
'Account Dunning'
__name__ = 'account.dunning'
company = fields.Many2One(
'company.company', "Company", required=True, states=_STATES,
help="Make the dunning belong to the company.")
line = fields.Many2One('account.move.line', 'Line', required=True,
2018-03-09 11:50:18 +01:00
help="The receivable line to dun for.",
2013-09-18 22:52:41 +02:00
domain=[
2019-03-24 18:00:01 +01:00
('account.type.receivable', '=', True),
2013-09-18 22:52:41 +02:00
('account.company', '=', Eval('company', -1)),
['OR',
('debit', '>', 0),
('credit', '<', 0),
],
],
states=_STATES)
2013-09-18 22:52:41 +02:00
procedure = fields.Many2One('account.dunning.procedure', 'Procedure',
required=True, states=_STATES)
2013-09-18 22:52:41 +02:00
level = fields.Many2One('account.dunning.level', 'Level', required=True,
domain=[
('procedure', '=', Eval('procedure', -1)),
],
states=_STATES)
2021-09-23 23:10:33 +02:00
date = fields.Date(
"Date", readonly=True,
states={
'invisible': Eval('state') == 'draft',
},
help="When the dunning reached the level.")
age = fields.Function(fields.TimeDelta(
"Age",
states={
'invisible': Eval('state') == 'draft',
},
help="How long the dunning has been at the level."),
'get_age')
2018-03-09 11:50:18 +01:00
blocked = fields.Boolean('Blocked',
help="Check to block further levels of the procedure.")
2013-09-18 22:52:41 +02:00
state = fields.Selection([
('draft', 'Draft'),
2018-08-31 14:48:27 +02:00
('waiting', "Waiting"),
('final', "Final"),
2022-02-15 00:08:46 +01:00
], 'State', readonly=True, sort=False)
2013-09-18 22:52:41 +02:00
active = fields.Function(fields.Boolean('Active'), 'get_active',
searcher='search_active')
party = fields.Function(fields.Many2One(
'party.party', 'Party',
context={
'company': Eval('company', -1),
},
depends={'company'}),
2013-09-18 22:52:41 +02:00
'get_line_field', searcher='search_line_field')
2021-08-21 09:09:55 +02:00
amount = fields.Function(Monetary(
"Amount", currency='currency', digits='currency'),
2013-09-18 22:52:41 +02:00
'get_amount')
2021-08-20 23:45:02 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"), 'get_line_field')
2013-09-18 22:52:41 +02:00
maturity_date = fields.Function(fields.Date('Maturity Date'),
'get_line_field', searcher='search_line_field')
2021-08-21 09:09:55 +02:00
amount_second_currency = fields.Function(Monetary(
2013-09-18 22:52:41 +02:00
'Amount Second Currency',
2021-08-21 09:09:55 +02:00
currency='second_currency', digits='second_currency',
2021-08-20 23:45:02 +02:00
states={
'invisible': Eval('currency') == Eval('second_currency'),
}),
2021-08-20 23:45:02 +02:00
'get_amount_second_currency')
2013-09-18 22:52:41 +02:00
second_currency = fields.Function(fields.Many2One('currency.currency',
'Second Currency'), 'get_second_currency')
@classmethod
def __setup__(cls):
super(Dunning, cls).__setup__()
table = cls.__table__()
cls._sql_constraints = [
('line_unique', Unique(table, table.line),
2018-12-29 14:20:28 +01:00
'account_dunning.msg_dunning_line_unique'),
]
cls._sql_indexes.add(
Index(
table,
(table.state, Index.Equality()),
where=table.state.in_(['draft', 'waiting'])))
2018-03-26 14:08:59 +02:00
cls._active_field = 'active'
2021-08-20 23:45:02 +02:00
cls._buttons.update({
'reschedule': {
'invisible': ~Eval('active', True),
'depends': ['active'],
},
})
2013-09-18 22:52:41 +02:00
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_state():
return 'draft'
2021-09-23 23:10:33 +02:00
def get_age(self, name):
pool = Pool()
Date = pool.get('ir.date')
if self.date:
with Transaction().set_context(company=self.company.id):
return Date.today() - self.date
2021-09-23 23:10:33 +02:00
@classmethod
def order_age(cls, tables):
pool = Pool()
Date = pool.get('ir.date')
table, _ = tables[None]
return [Literal(Date.today()) - table.date]
2013-09-18 22:52:41 +02:00
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):
2017-09-27 18:37:25 +02:00
return [('line.' + clause[0],) + tuple(clause[1:])]
2013-09-18 22:52:41 +02:00
def get_amount(self, name):
return self.line.debit - self.line.credit
2013-09-18 22:52:41 +02:00
def get_amount_second_currency(self, name):
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
2013-09-18 22:52:41 +02:00
def get_second_currency(self, name):
if self.line.second_currency:
return self.line.second_currency.id
else:
return self.line.account.company.currency.id
2013-09-18 22:52:41 +02:00
@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 [
2019-03-24 18:00:01 +01:00
('account.type.receivable', '=', True),
2013-09-18 22:52:41 +02:00
('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)
with check_access():
dunnings = cls.search([
2018-08-31 14:48:27 +02:00
('state', '=', 'waiting'),
2013-09-18 22:52:41 +02:00
('blocked', '=', False),
])
dunnings = cls.browse(dunnings)
for dunning in dunnings:
2013-09-18 22:52:41 +02:00
procedure = dunning.procedure
levels = procedure.levels
levels = levels[levels.index(dunning.level) + 1:]
2018-08-31 14:48:27 +02:00
if levels:
for level in levels:
if level.test(dunning.line, date):
break
else:
level = dunning.level
if level != dunning.level:
set_level[level].append(dunning)
2013-09-18 22:52:41 +02:00
else:
2018-08-31 14:48:27 +02:00
set_level[None].append(dunning)
2018-03-09 11:48:37 +01:00
to_write = []
2018-06-26 19:11:41 +02:00
for level, dunnings in set_level.items():
2018-08-31 14:48:27 +02:00
if level:
to_write.extend((dunnings, {
'level': level.id,
'state': 'draft',
2021-09-23 23:10:33 +02:00
'date': None,
2018-08-31 14:48:27 +02:00
}))
else:
to_write.extend((dunnings, {
'state': 'final',
2021-09-23 23:10:33 +02:00
'date': Date.today(),
2018-08-31 14:48:27 +02:00
}))
2018-03-09 11:48:37 +01:00
if to_write:
cls.write(*to_write)
2013-09-18 22:52:41 +02:00
with check_access():
lines = MoveLine.search(cls._overdue_line_domain(date))
lines = MoveLine.browse(lines)
2013-09-18 22:52:41 +02:00
dunnings = (cls._get_dunning(line, date) for line in lines)
2014-11-01 11:53:47 +01:00
cls.save([d for d in dunnings if d])
2013-09-18 22:52:41 +02:00
@classmethod
def _get_dunning(cls, line, date):
2015-03-16 13:07:21 +01:00
procedure = line.dunning_procedure
2013-09-18 22:52:41 +02:00
if not procedure:
return
for level in procedure.levels:
if level.test(line, date):
break
else:
return
return cls(
company=line.account.company,
2013-09-18 22:52:41 +02:00
line=line,
procedure=procedure,
level=level,
)
@classmethod
def process(cls, dunnings):
2021-09-23 23:10:33 +02:00
pool = Pool()
Date = pool.get('ir.date')
for company, c_dunnings in groupby(dunnings, key=lambda d: d.company):
with Transaction().set_context(company=company.id):
today = Date.today()
cls.write([d for d in c_dunnings
if not d.blocked and d.state == 'draft'], {
'state': 'waiting',
'date': today,
})
2013-09-18 22:52:41 +02:00
2021-08-20 23:45:02 +02:00
@classmethod
@ModelView.button_action('account_dunning.act_reschedule_dunning_wizard')
def reschedule(cls, dunnings):
pass
2013-09-18 22:52:41 +02:00
class CreateDunningStart(ModelView):
'Create Account Dunning'
__name__ = 'account.dunning.create.start'
2018-03-09 11:50:18 +01:00
date = fields.Date('Date', required=True,
help="Create dunning up to this date.")
2013-09-18 22:52:41 +02:00
@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'),
2013-10-23 12:01:16 +02:00
Button('Process', 'process', 'tryton-ok', default=True),
2013-09-18 22:52:41 +02:00
])
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):
self.model.process(self.records)
2013-09-18 22:52:41 +02:00
return self.next_state('process')
2021-08-20 23:45:02 +02:00
class RescheduleDunning(Wizard):
"Reschedule Account Dunning"
__name__ = 'account.dunning.reschedule'
2022-01-26 00:32:20 +01:00
start = StateAction('account.act_reschedule_lines_wizard')
2021-08-20 23:45:02 +02:00
def do_start(self, action):
return action, {
'ids': [self.record.line.id],
'model': 'account.move.line',
}