# 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 DeactivableMixin, ModelSQL, ModelView, Unique, fields from trytond.modules.currency.fields import Monetary from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval from trytond.transaction import Transaction class Fee(DeactivableMixin, ModelSQL, ModelView): __name__ = 'account.dunning.fee' name = fields.Char('Name', required=True, translate=True) product = fields.Many2One('product.product', 'Product', required=True, domain=[ ('type', '=', 'service'), ('template.type', '=', 'service'), ]) journal = fields.Many2One('account.journal', 'Journal', required=True) compute_method = fields.Selection([ ('list_price', 'List Price'), ('percentage', 'Percentage'), ], 'Compute Method', required=True, help='Method to compute the fee amount') percentage = fields.Numeric( "Percentage", digits=(None, 8), states={ 'invisible': Eval('compute_method') != 'percentage', 'required': Eval('compute_method') == 'percentage', }) def get_list_price(self, dunning): pool = Pool() Product = pool.get('product.product') with Transaction().set_context(company=dunning.company.id): product = Product(self.product) return product.list_price_used def get_amount(self, dunning): 'Return fee amount and currency' amount, currency = None, None if self.compute_method == 'list_price': currency = dunning.company.currency amount = currency.round(self.get_list_price(dunning)) elif self.compute_method == 'percentage': if dunning.second_currency: amount = dunning.amount_second_currency currency = dunning.second_currency else: amount = dunning.amount currency = dunning.company.currency amount = currency.round(amount * self.percentage) return amount, currency class Level(metaclass=PoolMeta): __name__ = 'account.dunning.level' fee = fields.Many2One('account.dunning.fee', 'Fee') class Dunning(metaclass=PoolMeta): __name__ = 'account.dunning' fees = fields.One2Many( 'account.dunning.fee.dunning_level', 'dunning', 'Fees', readonly=True) @classmethod def process(cls, dunnings): pool = Pool() FeeDunningLevel = pool.get('account.dunning.fee.dunning_level') fees = [] for dunning in dunnings: if dunning.blocked or not dunning.level.fee: continue if dunning.level in {f.level for f in dunning.fees}: continue fee = FeeDunningLevel(dunning=dunning, level=dunning.level) fee.amount, fee.currency = dunning.level.fee.get_amount(dunning) fees.append(fee) FeeDunningLevel.save(fees) FeeDunningLevel.process(fees) super().process(dunnings) class FeeDunningLevel(ModelSQL, ModelView): __name__ = 'account.dunning.fee.dunning_level' dunning = fields.Many2One( 'account.dunning', "Dunning", required=True) level = fields.Many2One('account.dunning.level', 'Level', required=True) amount = Monetary( "Amount", currency='currency', digits='currency') currency = fields.Many2One('currency.currency', 'Currency') moves = fields.One2Many('account.move', 'origin', 'Moves', readonly=True) @classmethod def __setup__(cls): super().__setup__() t = cls.__table__() cls._sql_constraints = [ ('dunning_level_unique', Unique(t, t.dunning, t.level), 'account_dunning_fee.msg_fee_dunning_level_unique'), ] def get_rec_name(self, name): return '%s @ %s' % (self.dunning.rec_name, self.level.rec_name) @classmethod def search_rec_name(cls, name, clause): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [bool_op, ('dunning.rec_name', *clause[1:]), ('level.rec_name', *clause[1:]), ] @classmethod def process(cls, fees): pool = Pool() Move = pool.get('account.move') moves = [] for fee in fees: move = fee.get_move_process() moves.append(move) Move.save(moves) Move.post(moves) def get_move_process(self): pool = Pool() Move = pool.get('account.move') Line = pool.get('account.move.line') Date = pool.get('ir.date') Period = pool.get('account.period') Currency = pool.get('currency.currency') with Transaction().set_context(company=self.dunning.company.id): today = Date.today() move = Move() move.company = self.dunning.company move.journal = self.level.fee.journal move.date = today move.period = Period.find(move.company, date=today) move.origin = self move.description = self.level.fee.name line = Line() if self.currency == move.company.currency: line.debit = self.amount else: line.second_currency = self.currency line.amount_second_currency = self.amount line.debit = Currency.compute( self.currency, self.amount, move.company.currency) line.account = self.dunning.line.account line.party = self.dunning.line.party counterpart = Line() counterpart.credit = line.debit counterpart.account = self.level.fee.product.account_revenue_used if counterpart.account and counterpart.account.party_required: counterpart.party = self.dunning.party move.lines = [line, counterpart] return move # TODO create move with taxes on reconcile of process move line class Letter(metaclass=PoolMeta): __name__ = 'account.dunning.letter' @classmethod def get_party_letter(cls): PartyLetter = super().get_party_letter() class PartyLetterFee(PartyLetter): @property def fees(self): fees = defaultdict(int) fees.update(super().fees) for dunning in self.dunnings: for fee in dunning.fees: fees[fee.currency] += fee.amount return fees return PartyLetterFee