# 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 __future__ import unicode_literals from decimal import Decimal from itertools import groupby from simpleeval import simple_eval try: from sql import Null except ImportError: Null = None from sql.aggregate import Sum from trytond.model import ModelView, ModelSQL, MatchMixin, fields from trytond.pyson import Eval, Bool, If, Id, PYSONEncoder from trytond.tools import decistmt, grouped_slice, reduce_ids from trytond.pool import Pool from trytond.wizard import Wizard, StateView, StateAction, Button from trytond.transaction import Transaction from trytond.modules.product import price_digits __all__ = ['Agent', 'Plan', 'PlanLines', 'Commission', 'CreateInvoice', 'CreateInvoiceAsk'] class Agent(ModelSQL, ModelView): 'Commission Agent' __name__ = 'commission.agent' party = fields.Many2One('party.party', 'Party', required=True) type_ = fields.Selection([ ('agent', 'Agent Of'), ('principal', 'Principal Of'), ], 'Type') company = fields.Many2One('company.company', 'Company', required=True) plan = fields.Many2One('commission.plan', 'Plan') currency = fields.Many2One('currency.currency', 'Currency', states={ 'required': Bool(Eval('plan')), 'invisible': ~Eval('plan'), }, depends=['plan']) pending_amount = fields.Function(fields.Numeric('Pending Amount', digits=price_digits), 'get_pending_amount') @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_type_(): return 'agent' @fields.depends('company', 'currency') def on_change_company(self): if self.company and not self.currency: self.currency = self.company.currency def get_rec_name(self, name): if self.plan: return '%s - %s' % (self.party.rec_name, self.plan.rec_name) else: return self.party.rec_name @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [bool_op, ('party',) + tuple(clause[1:]), ('plan',) + tuple(clause[1:]), ] @classmethod def get_pending_amount(cls, agents, name): pool = Pool() Commission = pool.get('commission') commission = Commission.__table__() cursor = Transaction().cursor ids = [a.id for a in agents] amounts = dict.fromkeys(ids, None) for sub_ids in grouped_slice(ids): where = reduce_ids(commission.agent, sub_ids) where &= commission.invoice_line == Null query = commission.select(commission.agent, Sum(commission.amount), where=where, group_by=commission.agent) cursor.execute(*query) amounts.update(dict(cursor.fetchall())) digits = cls.pending_amount.digits exp = Decimal(str(10.0 ** -digits[1])) for agent_id, amount in amounts.iteritems(): if amount: # SQLite uses float for SUM if not isinstance(amount, Decimal): amount = Decimal(str(amount)) amounts[agent_id] = amount.quantize(exp) return amounts @property def account(self): if self.type_ == 'agent': return self.party.account_payable elif self.type_ == 'principal': return self.party.account_receivable class Plan(ModelSQL, ModelView): 'Commission Plan' __name__ = 'commission.plan' name = fields.Char('Name', required=True, translate=True) commission_product = fields.Many2One('product.product', 'Commission Product', required=True, domain=[ ('type', '=', 'service'), ('default_uom', '=', Id('product', 'uom_unit')), ('template.type', '=', 'service'), ('template.default_uom', '=', Id('product', 'uom_unit')), ]) commission_method = fields.Selection([ ('posting', 'On Posting'), ('payment', 'On Payment'), ], 'Commission Method', help='When the commission is due') lines = fields.One2Many('commission.plan.line', 'plan', 'Lines') @staticmethod def default_commission_method(): return 'posting' def get_context_formula(self, amount, product): return { 'names': { 'amount': amount, }, } def compute(self, amount, product, pattern=None): 'Compute commission amount for the amount' if pattern is None: pattern = {} pattern['product'] = product.id if product else None context = self.get_context_formula(amount, product) for line in self.lines: if line.match(pattern): return line.get_amount(**context) class PlanLines(ModelSQL, ModelView, MatchMixin): 'Commission Plan Line' __name__ = 'commission.plan.line' plan = fields.Many2One('commission.plan', 'Plan', required=True, ondelete='CASCADE') product = fields.Many2One('product.product', 'Product') sequence = fields.Integer('Sequence') formula = fields.Char('Formula', required=True, help=('Python expression that will be evaluated with:\n' '- amount: the original amount')) @classmethod def __setup__(cls): super(PlanLines, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) cls._error_messages.update({ 'invalid_formula': ('Invalid formula "%(formula)s" in ' 'commission plan line "%(line)s" with exception ' '"%(exception)s".'), }) @staticmethod def order_sequence(tables): table, _ = tables[None] return [table.sequence == Null, table.sequence] @staticmethod def default_formula(): return 'amount' @classmethod def validate(cls, lines): super(PlanLines, cls).validate(lines) for line in lines: line.check_formula() def check_formula(self): context = self.plan.get_context_formula(Decimal(0), None) try: if not isinstance(self.get_amount(**context), Decimal): raise ValueError except ValueError, exception: self.raise_user_error('invalid_formula', { 'formula': self.formula, 'line': self.rec_name, 'exception': exception, }) def get_amount(self, **context): 'Return amount (as Decimal)' context.setdefault('functions', {})['Decimal'] = Decimal return simple_eval(decistmt(self.formula), **context) class Commission(ModelSQL, ModelView): 'Commission' __name__ = 'commission' _readonly_states = { 'readonly': Bool(Eval('invoice_line')), } _readonly_depends = ['invoice_line'] origin = fields.Reference('Origin', selection='get_origin', select=True, readonly=True) date = fields.Date('Date', select=True, states=_readonly_states, depends=_readonly_depends) agent = fields.Many2One('commission.agent', 'Agent', required=True, states=_readonly_states, depends=_readonly_depends) product = fields.Many2One('product.product', 'Product', required=True, states=_readonly_states, depends=_readonly_depends) amount = fields.Numeric('Amount', required=True, digits=price_digits, domain=[('amount', '!=', 0)], states=_readonly_states, depends=_readonly_depends) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') type_ = fields.Function(fields.Selection([ ('in', 'Incoming'), ('out', 'Outgoing'), ], 'Type'), 'on_change_with_type_') invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', readonly=True, depends=['amount', 'type_']) invoice_state = fields.Function(fields.Selection([ ('', ''), ('invoiced', 'Invoiced'), ('paid', 'Paid'), ('cancel', 'Canceled'), ], 'Invoice State'), 'get_invoice_state') @classmethod def __setup__(cls): super(Commission, cls).__setup__() cls._buttons.update({ 'invoice': { 'invisible': Bool(Eval('invoice_line')), }, }) @classmethod def _get_origin(cls): 'Return list of Model names for origin Reference' return ['account.invoice.line'] @classmethod def get_origin(cls): pool = Pool() Model = pool.get('ir.model') models = cls._get_origin() models = Model.search([ ('model', 'in', models), ]) return [(None, '')] + [(m.model, m.name) for m in models] @fields.depends('agent') def on_change_with_currency(self, name=None): if self.agent: return self.agent.currency.id @fields.depends('agent') def on_change_with_type_(self, name=None): if self.agent: return { 'agent': 'out', 'principal': 'in', }.get(self.agent.type_) def get_invoice_state(self, name): state = '' if self.invoice_line: state = 'invoiced' invoice = self.invoice_line.invoice if invoice: if invoice.state == 'paid': state = 'paid' elif invoice.state == 'cancel': state = 'cancel' return state @classmethod def copy(cls, commissions, default=None): if default is None: default = {} default.setdefault('invoice_line', None) return super(Commission, cls).copy(commissions, default=default) @classmethod @ModelView.button def invoice(cls, commissions): pool = Pool() Invoice = pool.get('account.invoice') key = lambda c: c._group_to_invoice_key() commissions.sort(key=key) invoices = [] to_write = [] for key, commissions in groupby(commissions, key=key): commissions = list(commissions) key = dict(key) invoice = cls._get_invoice(key) invoice.save() invoices.append(invoice) key = lambda c: c._group_to_invoice_line_key() commissions.sort(key=key) for key, commissions in groupby(commissions, key=key): commissions = [c for c in commissions if not c.invoice_line] key = dict(key) invoice_line = cls._get_invoice_line(key, invoice, commissions) invoice_line.save() to_write.extend([commissions, { 'invoice_line': invoice_line.id, }]) if to_write: cls.write(*to_write) Invoice.update_taxes(invoices) def _group_to_invoice_key(self): direction = { 'in': 'out', 'out': 'in', }.get(self.type_) document = 'invoice' if self.amount > 0 else 'credit_note' return (('agent', self.agent), ('type', '%s_%s' % (direction, document)), ) @classmethod def get_journal(cls): pool = Pool() Journal = pool.get('account.journal') journals = Journal.search([ ('type', '=', 'commission'), ], limit=1) if journals: return journals[0] @classmethod def _get_invoice(cls, key): pool = Pool() Invoice = pool.get('account.invoice') agent = key['agent'] if key['type'].startswith('out'): payment_term = agent.party.customer_payment_term else: payment_term = agent.party.supplier_payment_term return Invoice( company=agent.company, type=key['type'], journal=cls.get_journal(), party=agent.party, invoice_address=agent.party.address_get(type='invoice'), currency=agent.currency, account=agent.account, payment_term=payment_term, ) def _group_to_invoice_line_key(self): return (('product', self.product),) @classmethod def _get_invoice_line(cls, key, invoice, commissions): pool = Pool() InvoiceLine = pool.get('account.invoice.line') def sign(commission): if invoice.type.startswith(commission.type_): return -1 else: return 1 product = key['product'] amount = invoice.currency.round( sum(c.amount * sign(c) for c in commissions)) if invoice.type.endswith('credit_note'): amount *= -1 invoice_line = InvoiceLine() invoice_line.invoice = invoice invoice_line.type = 'line' invoice_line.product = product invoice_line.quantity = 1 invoice_line.on_change_product() invoice_line.unit_price = amount return invoice_line class CreateInvoice(Wizard): 'Create Commission Invoice' __name__ = 'commission.create_invoice' start_state = 'ask' ask = StateView('commission.create_invoice.ask', 'commission.commission_create_invoice_ask_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('OK', 'create_', 'tryton-ok', default=True), ]) create_ = StateAction('account_invoice.act_invoice_form') def get_domain(self): domain = [('invoice_line', '=', None)] if self.ask.from_: domain.append(('date', '>=', self.ask.from_)) if self.ask.to: domain.append(('date', '<=', self.ask.to)) if self.ask.type_ == 'in': domain.append(('agent.type_', '=', 'principal')) elif self.ask.type_ == 'out': domain.append(('agent.type_', '=', 'agent')) return domain def do_create_(self, action): pool = Pool() Commission = pool.get('commission') commissions = Commission.search(self.get_domain(), order=[('agent', 'DESC'), ('date', 'DESC')]) Commission.invoice(commissions) invoice_ids = list({c.invoice_line.invoice.id for c in commissions}) encoder = PYSONEncoder() action['pyson_domain'] = encoder.encode( [('id', 'in', invoice_ids)]) action['pyson_search_value'] = encoder.encode([]) return action, {} class CreateInvoiceAsk(ModelView): 'Create Commission Invoice' __name__ = 'commission.create_invoice.ask' from_ = fields.Date('From', domain=[ If(Bool(Eval('to')), [('from', '<=', Eval('to'))], []), ], depends=['to']) to = fields.Date('To', domain=[ If(Bool(Eval('from_')), [('to', '>=', Eval('from_'))], []), ], depends=['from_']) type_ = fields.Selection([ ('in', 'Incoming'), ('out', 'Outgoing'), ('both', 'Both'), ], 'Type') @staticmethod def default_type_(): return 'both'