from decimal import Decimal from trytond.model import ModelSQL, ModelView, fields from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval, Bool from trytond.modules.product import price_digits class Expense(ModelSQL, ModelView): 'Projecte Expense' __name__ = 'project.expense' _states = { 'readonly': Bool(Eval('invoice_line')), } _depends = ['invoice_line'] work = fields.Many2One('project.work', 'Work', required=True, states=_states, depends=_depends) origin = fields.Reference('Origin', selection='get_origin', readonly=True) product = fields.Many2One('product.product', 'Product', required=True, states=_states, depends=_depends) uom = fields.Many2One('product.uom', 'UoM', required=True, states=_states, depends=_depends) uom_category = fields.Function(fields.Many2One('product.uom.category', 'UoM Category'), 'on_change_with_uom_category') uom_digits = fields.Function(fields.Integer('UoM Digits'), 'on_change_with_uom_digits') cost_price = fields.Numeric('Cost Price', digits=price_digits, states=_states, depends=_depends) # TODO: Show field and apply price list unit_price = fields.Numeric('Unit Price', digits=price_digits, states=_states, depends=_depends) # TODO: Add currency digits #amount = fields.Function(fields.Numeric('Amount'), '') quantity = fields.Float('Quantity', digits=(16, Eval('uom_digits', 2)), required=True, states=_states, depends=_depends + ['uom_digits']) description = fields.Char('Description', states=_states, depends=_depends) invoiceable = fields.Selection([ (None, ''), ('yes', 'Yes'), ('no', 'No'), ], 'Invoiceable', states=_states, depends=_depends) invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', readonly=True) del _states # TODO: Add expense cost to the task/project @classmethod def _get_origin(cls): 'Return list of Model names for origin Reference' pool = Pool() res = [] for model in ('purchase.line', 'stock.move'): try: pool.get(model) res.append(model) except KeyError: pass return res @classmethod def get_origin(cls): 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('product') def on_change_product(self): if not self.product: return self.uom = self.product.default_uom @fields.depends('product') def on_change_with_uom_category(self, name=None): if not self.product: return return self.product.uom.category.id @fields.depends('uom') def on_change_with_uom_digits(self, name=None): if self.uom: return self.uom.digits return 2 def _get_invoice_lines(self): pool = Pool() try: AnalyticAccountEntry = pool.get('analytic.account.entry') except KeyError: AnalyticAccountEntry = None if not self.invoiceable == 'yes' or not self.quantity: # TODO: Raise a UserError if invoiceable is empty return [] line = { 'product': self.product, 'quantity': self.quantity, 'unit': self.uom, 'unit_price': self.unit_price or Decimal(0), 'origins': [self], 'description': self.description, } if AnalyticAccountEntry: if self.work.analytic_accounts: new_entries = AnalyticAccountEntry.copy( self.work.analytic_accounts, default={ 'origin': None, }) line['analytic_accounts'] = new_entries return [line] class Project(metaclass=PoolMeta): __name__ = 'project.work' expenses = fields.One2Many('project.expense', 'work', 'Expenses') @classmethod def __setup__(cls): super().__setup__() cls._buttons.update({ 'sync_expenses': { } }) @classmethod @ModelView.button def sync_expenses(cls, works): pool = Pool() Expense = pool.get('project.expense') Uom = pool.get('product.uom') to_save = [] to_delete = [] for work in works: expenses = work._get_expenses() expenses = {x.origin: x for x in expenses} for existing in work.expenses: if not existing.origin: continue expense = expenses.get(existing.origin) if not expense: to_delete.append(existing) continue existing_qty = Uom.compute_qty(existing.uom, existing.quantity, expense.uom) expense.quantity -= existing_qty for expense in expenses.values(): if not expense.quantity: continue to_save.append(expense) Expense.save(to_save) Expense.delete(to_delete) def _get_expenses(self): expenses = [] if hasattr(self, 'moves'): for move in self.moves: expense = self._get_expense_move(move) if expense: expenses.append(expense) if hasattr(self, 'purchase_lines'): for line in self.purchase_lines: expense = self._get_expense_purchase_line(line) if expense: expenses.append(expense) return expenses def _get_expense_move(self, move): Expense = Pool().get('project.expense') if move.state != 'done': return expense = Expense() expense.work = self expense.origin = move expense.product = move.product expense.uom = move.uom expense.quantity = move.quantity expense.cost_price = move.cost_price return expense def _get_expense_purchase_line(self, purchase_line): Expense = Pool().get('project.expense') expense = Expense() expense.work = self expense.origin = purchase_line expense.product = purchase_line.product expense.uom = purchase_line.unit expense.quantity = purchase_line.quantity expense.cost_price = purchase_line.unit_price return expense def _get_lines_to_invoice(self): lines = super()._get_lines_to_invoice() lines += self._get_expense_lines_to_invoice() return lines def _get_expense_lines_to_invoice(self): lines = [] for expense in self.expenses: lines += expense._get_invoice_lines() return lines