class Template: __metaclass__ = PoolMeta __name__ = 'product.template' depreciable = fields.Boolean('Depreciable', states={ 'readonly': ~Eval('active', True), 'invisible': Eval('type', '') != 'assets', }, depends=['active', 'type']) account_depreciation = fields.MultiValue( fields.Many2One( 'account.account', 'Account Depreciation', domain=[ ('kind', '=', 'other'), ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'readonly': ~Eval('active', True), 'required': ~Eval('accounts_category') & Eval('depreciable'), 'invisible': (~Eval('depreciable') | (Eval('type', '') != 'assets') | ~Eval('context', {}).get('company') | Eval('accounts_category')), }, depends=['active', 'depreciable', 'type', 'accounts_category'])) account_asset = fields.MultiValue( fields.Many2One( 'account.account', 'Account Asset', domain=[ ('kind', '=', 'expense'), ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'readonly': ~Eval('active', True), 'required': ~Eval('accounts_category') & Eval('depreciable'), 'invisible': (~Eval('depreciable') | (Eval('type', '') != 'assets') | ~Eval('context', {}).get('company') | Eval('accounts_category')), }, depends=['active', 'depreciable', 'type', 'accounts_category'])) depreciation_duration = fields.Integer( "Depreciation Duration", states={ 'readonly': ~Eval('active', True), 'invisible': (~Eval('depreciable') | (Eval('type', '') != 'assets')), }, depends=['active', 'depreciable', 'type'], help='In months') @classmethod def multivalue_model(cls, field): pool = Pool() if field in {'account_depreciation', 'account_asset'}: return pool.get('product.template.account') return super(Template, cls).multivalue_model(field) @property @account_used('account_depreciation') def account_depreciation_used(self): pass @property @account_used('account_asset') def account_asset_used(self): pass
class _Action_Line: __slots__ = () _states = { 'readonly': ((Eval('complaint_state') != 'draft') | Bool(Eval('_parent_action.result', True))), } action = fields.Many2One('sale.complaint.action', 'Action', ondelete='CASCADE', select=True, required=True) quantity = fields.Float("Quantity", digits='unit', states=_states) unit = fields.Function(fields.Many2One('product.uom', "Unit"), 'on_change_with_unit') unit_price = Monetary("Unit Price", currency='currency', digits=price_digits, states=_states, help='Leave empty for the same price.') amount = fields.Function( Monetary("Amount", currency='currency', digits='currency'), 'on_change_with_amount') currency = fields.Function( fields.Many2One('currency.currency', "Currency"), 'on_change_with_currency') complaint_state = fields.Function( fields.Selection('get_complaint_states', "Complaint State"), 'on_change_with_complaint_state') complaint_origin_id = fields.Function( fields.Integer("Complaint Origin ID"), 'on_change_with_complaint_origin_id') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('action') def on_change_with_unit(self, name=None): raise NotImplementedError @fields.depends('currency', methods=['get_quantity', 'get_unit_price']) def on_change_with_amount(self, name=None): quantity = self.get_quantity() or 0 unit_price = self.get_unit_price() or Decimal(0) amount = Decimal(str(quantity)) * unit_price if self.currency: amount = self.currency.round(amount) return amount def get_quantity(self): raise NotImplementedError def get_unit_price(self): raise NotImplementedError @fields.depends('action', '_parent_action.currency') def on_change_with_currency(self, name=None): if self.action and self.action.currency: return self.action.currency.id @classmethod def get_complaint_states(cls): pool = Pool() Complaint = pool.get('sale.complaint') return Complaint.fields_get(['state'])['state']['selection'] @fields.depends('action', '_parent_action.complaint', '_parent_action._parent_complaint.state') def on_change_with_complaint_state(self, name=None): if self.action and self.action.complaint: return self.action.complaint.state @fields.depends('action', '_parent_action.complaint', '_parent_action._parent_complaint.origin_id') def on_change_with_complaint_origin_id(self, name=None): if self.action and self.action.complaint: return self.action.complaint.origin_id
class Party(metaclass=PoolMeta): __name__ = 'party.party' credit_amount = fields.Function( fields.Numeric('Credit Amount', digits=(16, Eval('credit_limit_digits', 2)), depends=['credit_limit_digits']), 'get_credit_amount') credit_limit_amount = fields.MultiValue( fields.Numeric('Credit Limit Amount', digits=(16, Eval('credit_limit_digits', 2)), depends=['credit_limit_digits'])) credit_limit_digits = fields.Function(fields.Integer('Currency Digits'), 'get_credit_limit_digits') credit_limit_amounts = fields.One2Many('party.party.credit_limit_amount', 'party', "Credit Limit Amounts") @classmethod def default_credit_limit_amount(cls, **pattern): pool = Pool() Configuration = pool.get('account.configuration') config = Configuration(1) return config.get_multivalue('default_credit_limit_amount', **pattern) @classmethod def get_credit_amount(cls, parties, name): return {p.id: p.receivable for p in parties} @staticmethod def _credit_limit_to_lock(): 'Return models to lock when checking credit limit' return ['account.move.line'] def check_credit_limit(self, amount, origin=None): ''' Check if amount will not reach credit limit for party If origin is set and user is in group credit_limit then a warning will be raised ''' pool = Pool() ModelData = pool.get('ir.model.data') try: Dunning = pool.get('account.dunning') except KeyError: Dunning = None User = pool.get('res.user') Group = pool.get('res.group') Company = pool.get('company.company') Lang = pool.get('ir.lang') Warning = pool.get('res.user.warning') if self.credit_limit_amount is None: return def in_group(): group = Group( ModelData.get_id('account_credit_limit', 'group_credit_limit')) transaction = Transaction() user_id = transaction.user if user_id == 0: user_id = transaction.context.get('user', user_id) if user_id == 0: return True user = User(user_id) return origin and group in user.groups for model in self._credit_limit_to_lock(): Model = pool.get(model) Transaction().database.lock(Transaction().connection, Model._table) if self.credit_limit_amount < self.credit_amount + amount: company = Company(Transaction().context.get('company')) lang = Lang.get() limit = lang.currency(self.credit_limit_amount, company.currency) if not in_group(): raise CreditLimitError( gettext( 'account_credit_limit' '.msg_party_credit_limit_amount', party=self.rec_name, limit=limit)) warning_name = 'credit_limit_amount_%s' % origin if Warning.check(warning_name): raise CreditLimitWarning( warning_name, gettext( 'account_credit_limit' '.msg_party_credit_limit_amount', party=self.rec_name, limit=limit)) if Dunning: dunnings = Dunning.search([ ('party', '=', self.id), ('level.credit_limit', '=', True), ('blocked', '!=', True), ]) if dunnings: dunning = dunnings[0] if not in_group(): raise CreditLimitError( gettext( 'account_credit_limit' '.msg_party_credit_limit_dunning', party=self.rec_name, dunning=dunning.rec_name)) warning_name = 'credit_limit_dunning_%s' % origin if Warning.check(warning_name): raise CreditLimitWarning( warning_name, gettext( 'account_credit_limit' '.msg_party_credit_limit_dunning', party=self.rec_name, dunning=dunning.rec_name)) def get_credit_limit_digits(self, name): pool = Pool() Company = pool.get('company.company') company_id = Transaction().context.get('company') if company_id: company = Company(company_id) return company.currency.digits
class Statement(Workflow, ModelSQL, ModelView): 'Account Statement' __name__ = 'account.statement' _states = {'readonly': Eval('state') != 'draft'} _balance_states = _states.copy() _balance_states.update({ 'invisible': ~Eval('validation', '').in_(['balance']), 'required': Eval('validation', '').in_(['balance']), }) _amount_states = _states.copy() _amount_states.update({ 'invisible': ~Eval('validation', '').in_(['amount']), 'required': Eval('validation', '').in_(['amount']), }) _number_states = _states.copy() _number_states.update({ 'invisible': ~Eval('validation', '').in_(['number_of_lines']), 'required': Eval('validation', '').in_(['number_of_lines']), }) name = fields.Char('Name', required=True) company = fields.Many2One( 'company.company', "Company", required=True, select=True, states=_states) journal = fields.Many2One('account.statement.journal', 'Journal', required=True, select=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }) currency = fields.Function(fields.Many2One( 'currency.currency', "Currency"), 'on_change_with_currency') date = fields.Date('Date', required=True, select=True) start_balance = Monetary( "Start Balance", currency='currency', digits='currency', states=_balance_states) end_balance = Monetary( "End Balance", currency='currency', digits='currency', states=_balance_states) balance = fields.Function(Monetary( "Balance", currency='currency', digits='currency', states=_balance_states), 'on_change_with_balance') total_amount = Monetary( "Total Amount", currency='currency', digits='currency', states=_amount_states) number_of_lines = fields.Integer('Number of Lines', states=_number_states) lines = fields.One2Many('account.statement.line', 'statement', 'Lines', states={ 'readonly': (Eval('state') != 'draft') | ~Eval('journal'), }) origins = fields.One2Many('account.statement.origin', 'statement', "Origins", states={ 'readonly': Eval('state') != 'draft', }) origin_file = fields.Binary( "Origin File", readonly=True, file_id=file_id, store_prefix=store_prefix) origin_file_id = fields.Char("Origin File ID", readonly=True) state = fields.Selection([ ('draft', "Draft"), ('validated', "Validated"), ('cancelled', "Cancelled"), ('posted', "Posted"), ], "State", readonly=True, select=True, sort=False) validation = fields.Function(fields.Char('Validation'), 'on_change_with_validation') to_reconcile = fields.Function( fields.Boolean("To Reconcile"), 'get_to_reconcile') del _states del _balance_states del _amount_states del _number_states @classmethod def __setup__(cls): super(Statement, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._transitions |= set(( ('draft', 'validated'), ('draft', 'cancelled'), ('validated', 'posted'), ('validated', 'cancelled'), ('cancelled', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') != 'cancelled', 'depends': ['state'], }, 'validate_statement': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'post': { 'invisible': Eval('state') != 'validated', 'depends': ['state'], }, 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'validated']), 'depends': ['state'], }, 'reconcile': { 'invisible': Eval('state').in_(['draft', 'cancelled']), 'readonly': ~Eval('to_reconcile'), 'depends': ['state', 'to_reconcile'], }, }) cls.__rpc__.update({ 'post': RPC( readonly=False, instantiate=0, fresh_session=True), }) @classmethod def __register__(cls, module_name): transaction = Transaction() cursor = transaction.connection.cursor() sql_table = cls.__table__() super(Statement, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.2: remove required on start/end balance table.not_null_action('start_balance', action='remove') table.not_null_action('end_balance', action='remove') # Migration from 3.2: add required name cursor.execute(*sql_table.update([sql_table.name], [sql_table.id.cast(cls.name.sql_type().base)], where=sql_table.name == Null)) # Migration from 5.6: rename state cancel to cancelled cursor.execute(*sql_table.update( [sql_table.state], ['cancelled'], where=sql_table.state == 'cancel')) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @fields.depends('journal', 'state', 'lines') def on_change_journal(self): if not self.journal: return statements = self.search([ ('journal', '=', self.journal.id), ], order=[ ('date', 'DESC'), ('id', 'DESC'), ], limit=1) if not statements: return statement, = statements self.start_balance = statement.end_balance @fields.depends('journal') def on_change_with_currency(self, name=None): if self.journal: return self.journal.currency.id @fields.depends('start_balance', 'end_balance') def on_change_with_balance(self, name=None): return ((getattr(self, 'end_balance', 0) or 0) - (getattr(self, 'start_balance', 0) or 0)) @fields.depends('origins', 'lines', 'journal', 'company') def on_change_origins(self): if not self.journal or not self.origins or not self.company: return if self.journal.currency != self.company.currency: return invoices = set() for line in self.lines: if (line.invoice and line.invoice.currency == self.company.currency): invoices.add(line.invoice) for origin in self.origins: for line in origin.lines: if (line.invoice and line.invoice.currency == self.company.currency): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: if invoice.type == 'out': sign = -1 else: sign = 1 invoice_id2amount_to_pay[invoice.id] = sign * invoice.amount_to_pay origins = list(self.origins) for origin in origins: lines = list(origin.lines) for line in lines: if (line.invoice and line.id and line.invoice.id in invoice_id2amount_to_pay): amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] if (amount_to_pay and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): if abs(line.amount) > abs(amount_to_pay): line.amount = amount_to_pay.copy_sign(line.amount) else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: line.invoice = None origin.lines = lines self.origins = origins @fields.depends('lines', 'journal', 'company') def on_change_lines(self): pool = Pool() Line = pool.get('account.statement.line') if not self.journal or not self.lines or not self.company: return if self.journal.currency != self.company.currency: return invoices = set() for line in self.lines: if (line.invoice and line.invoice.currency == self.company.currency): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: if invoice.type == 'out': sign = -1 else: sign = 1 invoice_id2amount_to_pay[invoice.id] = sign * invoice.amount_to_pay lines = list(self.lines) line_offset = 0 for index, line in enumerate(self.lines or []): if line.invoice and line.id: if line.invoice.id not in invoice_id2amount_to_pay: continue amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] if (amount_to_pay and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): if abs(line.amount) > abs(amount_to_pay): new_line = Line() for field_name, field in Line._fields.items(): if field_name == 'id': continue try: setattr(new_line, field_name, getattr(line, field_name)) except AttributeError: pass new_line.amount = line.amount + amount_to_pay new_line.invoice = None line_offset += 1 lines.insert(index + line_offset, new_line) invoice_id2amount_to_pay[line.invoice.id] = 0 line.amount = amount_to_pay.copy_sign(line.amount) else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: line.invoice = None self.lines = lines @fields.depends('journal') def on_change_with_validation(self, name=None): if self.journal: return self.journal.validation def get_to_reconcile(self, name=None): return bool(self.lines_to_reconcile) @property def lines_to_reconcile(self): lines = [] for line in self.lines: if line.move: for move_line in line.move.lines: if (move_line.account.reconcile and not move_line.reconciliation): lines.append(move_line) return lines def _group_key(self, line): key = ( ('number', line.number or Unequal()), ('date', line.date), ('party', line.party), ) return key def _get_grouped_line(self): "Return Line class for grouped lines" lines = self.origins or self.lines assert lines keys = [k[0] for k in self._group_key(lines[0])] class Line(namedtuple('Line', keys + ['lines'])): @property def amount(self): return sum((l.amount for l in self.lines)) @property def descriptions(self): done = set() for line in self.lines: if line.description and line.description not in done: done.add(line.description) yield line.description return Line @property def grouped_lines(self): if self.origins: lines = self.origins elif self.lines: lines = self.lines else: return Line = self._get_grouped_line() for key, lines in groupby(lines, key=self._group_key): yield Line(**dict(key + (('lines', list(lines)),))) @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] @classmethod def delete(cls, statements): # Cancel before delete cls.cancel(statements) for statement in statements: if statement.state != 'cancelled': raise AccessError( gettext('account_statement.msg_statement_delete_cancel', statement=statement.rec_name)) super(Statement, cls).delete(statements) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, statements): pass def validate_balance(self): pool = Pool() Lang = pool.get('ir.lang') amount = (self.start_balance + sum(l.amount for l in self.lines)) if amount != self.end_balance: lang = Lang.get() end_balance = lang.currency( self.end_balance, self.journal.currency) amount = lang.currency(amount, self.journal.currency) raise StatementValidateError( gettext('account_statement.msg_statement_wrong_end_balance', statement=self.rec_name, end_balance=end_balance, amount=amount)) def validate_amount(self): pool = Pool() Lang = pool.get('ir.lang') amount = sum(l.amount for l in self.lines) if amount != self.total_amount: lang = Lang.get() total_amount = lang.currency( self.total_amount, self.journal.currency) amount = lang.currency(amount, self.journal.currency) raise StatementValidateError( gettext('account_statement.msg_statement_wrong_total_amount', statement=self.rec_name, total_amount=total_amount, amount=amount)) def validate_number_of_lines(self): number = len(list(self.grouped_lines)) if number > self.number_of_lines: raise StatementValidateError( gettext('account_statement' '.msg_statement_wrong_number_of_lines_remove', statement=self.rec_name, n=number - self.number_of_lines)) elif number < self.number_of_lines: raise StatementValidateError( gettext('account_statement' '.msg_statement_wrong_number_of_lines_remove', statement=self.rec_name, n=self.number_of_lines - number)) @classmethod @ModelView.button @Workflow.transition('validated') def validate_statement(cls, statements): pool = Pool() Line = pool.get('account.statement.line') Warning = pool.get('res.user.warning') paid_cancelled_invoice_lines = [] for statement in statements: getattr(statement, 'validate_%s' % statement.validation)() paid_cancelled_invoice_lines.extend(l for l in statement.lines if l.invoice and l.invoice.state in {'cancelled', 'paid'}) if paid_cancelled_invoice_lines: warning_key = Warning.format( 'statement_paid_cancelled_invoice_lines', paid_cancelled_invoice_lines) if Warning.check(warning_key): raise StatementValidateWarning(warning_key, gettext('account_statement' '.msg_statement_invoice_paid_cancelled')) Line.write(paid_cancelled_invoice_lines, { 'related_to': None, }) cls.create_move(statements) cls.write(statements, { 'state': 'validated', }) common_lines = [l for l in Line.search([ ('statement.state', '=', 'draft'), ('related_to.state', 'in', ['posted', 'paid'], 'account.invoice'), ]) if l.invoice.reconciled] if common_lines: warning_key = '_'.join(str(l.id) for l in common_lines) if Warning.check(warning_key): raise StatementValidateWarning(warning_key, gettext('account_statement' '.msg_statement_paid_invoice_draft')) Line.write(common_lines, { 'related_to': None, }) @classmethod def create_move(cls, statements): '''Create move for the statements and try to reconcile the lines. Returns the list of move, statement and lines ''' pool = Pool() Line = pool.get('account.statement.line') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') moves = [] for statement in statements: for key, lines in groupby( statement.lines, key=statement._group_key): lines = list(lines) key = dict(key) move = statement._get_move(key) moves.append((move, statement, lines)) Move.save([m for m, _, _ in moves]) to_write = [] for move, _, lines in moves: to_write.append(lines) to_write.append({ 'move': move.id, }) if to_write: Line.write(*to_write) move_lines = [] for move, statement, lines in moves: amount = 0 amount_second_currency = 0 for line in lines: move_line = line.get_move_line() move_line.move = move amount += move_line.debit - move_line.credit if move_line.amount_second_currency: amount_second_currency += move_line.amount_second_currency move_lines.append((move_line, line)) move_line = statement._get_move_line( amount, amount_second_currency, lines) move_line.move = move move_lines.append((move_line, None)) MoveLine.save([l for l, _ in move_lines]) Line.reconcile(move_lines) return moves def _get_move(self, key): 'Return Move for the grouping key' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') period_id = Period.find(self.company.id, date=key['date']) return Move( period=period_id, journal=self.journal.journal, date=key['date'], origin=self, company=self.company, description=str(key['number']), ) def _get_move_line(self, amount, amount_second_currency, lines): 'Return counterpart Move Line for the amount' pool = Pool() MoveLine = pool.get('account.move.line') if self.journal.currency != self.company.currency: second_currency = self.journal.currency amount_second_currency *= -1 else: second_currency = None amount_second_currency = None descriptions = {l.description for l in lines} if len(descriptions) == 1: description, = descriptions else: description = '' return MoveLine( debit=abs(amount) if amount < 0 else 0, credit=abs(amount) if amount > 0 else 0, account=self.journal.account, second_currency=second_currency, amount_second_currency=amount_second_currency, description=description, ) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, statements): pool = Pool() Lang = pool.get('ir.lang') StatementLine = pool.get('account.statement.line') for statement in statements: for origin in statement.origins: if origin.pending_amount: lang = Lang.get() amount = lang.currency( origin.pending_amount, statement.journal.currency) raise StatementPostError( gettext('account_statement' '.msg_statement_post_pending_amount', statement=statement.rec_name, amount=amount, origin=origin.rec_name)) # Write state to skip statement test on Move.post cls.write(statements, {'state': 'posted'}) lines = [l for s in statements for l in s.lines] StatementLine.post_move(lines) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, statements): StatementLine = Pool().get('account.statement.line') lines = [l for s in statements for l in s.lines] StatementLine.delete_move(lines) @classmethod @ModelView.button_action('account_statement.act_reconcile') def reconcile(cls, statements): pass @classmethod def copy(cls, statements, default=None): default = default.copy() if default is not None else {} new_statements = [] for origins, sub_statements in groupby( statements, key=lambda s: bool(s.origins)): sub_statements = list(sub_statements) sub_default = default.copy() if origins: sub_default.setdefault('lines') new_statements.extend(super().copy( statements, default=sub_default)) return new_statements
class Transfer(Workflow, ModelSQL, ModelView): "Transfer between Cash/Bank" __name__ = "cash_bank.transfer" company = fields.Many2One( 'company.company', 'Company', required=True, states={ 'readonly': True, }, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], select=True) date = fields.Date('Date', required=True, states=_STATES, depends=_DEPENDS) reference = fields.Char('Reference', size=None) description = fields.Char('Description', size=None) cash_bank_from = fields.Many2One( 'cash_bank.cash_bank', 'From', required=True, domain=[ ('company', '=', Eval('company')), ], states={ 'readonly': Or(Eval('state') != 'draft', Eval('documents')), }, depends=_DEPENDS + ['company', 'documents']) type_from = fields.Many2One( 'cash_bank.receipt_type', 'Type', required=True, domain=[ If(Bool(Eval('cash_bank_from')), [('cash_bank', '=', Eval('cash_bank_from'))], [('cash_bank', '=', -1)]), ('type', '=', 'out') ], states={ 'readonly': Or(Eval('state') != 'draft', Eval('documents')), }, depends=_DEPENDS + ['cash_bank_from', 'documents']) cash_bank_to = fields.Many2One( 'cash_bank.cash_bank', 'To', required=True, domain=[ ('company', '=', Eval('company')), If(Bool(Eval('cash_bank_from')), [('id', '!=', Eval('cash_bank_from'))], [('id', '=', -1)]), ], states={ 'readonly': Or(Eval('state') != 'draft', Eval('documents')), }, depends=_DEPENDS + ['company', 'cash_bank_from', 'documents']) type_to = fields.Many2One( 'cash_bank.receipt_type', 'Type', required=True, domain=[ If(Bool(Eval('cash_bank_to')), [('cash_bank', '=', Eval('cash_bank_to'))], [('cash_bank', '=', -1)]), ('type', '=', 'in') ], states={ 'readonly': Or(Eval('state') != 'draft', Eval('documents')), }, depends=_DEPENDS + ['cash_bank_to', 'documents']) currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': True, }, depends=['state', 'documents']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') cash = fields.Numeric('Cash', digits=(16, Eval('currency_digits', 2)), states=_STATES, depends=_DEPENDS + ['currency_digits']) documents = fields.Many2Many( 'cash_bank.document-cash_bank.transfer', 'transfer', 'document', 'Documents', domain=[ If(Bool(Eval('type_from')), [ If( Eval('state') != 'posted', [('convertion', '=', None), If( Eval('state') == 'draft', [ ('last_receipt.cash_bank.id', '=', Eval('cash_bank_from')), ], [('last_receipt.cash_bank.id', '=', Eval('cash_bank_to'))])], [('id', '!=', -1)]) ], [('id', '=', -1)]), ], states=_STATES, depends=_DEPENDS + ['type_from', 'cash_bank_from', 'cash_bank_to']) total_documents = fields.Function( fields.Numeric('Total Documents', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total_documents') total = fields.Function( fields.Numeric('Total', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total') receipt_from = fields.Many2One('cash_bank.receipt', 'Receipt From', readonly=True) receipt_to = fields.Many2One('cash_bank.receipt', 'Receipt To', readonly=True) state = fields.Selection(STATES, 'State', readonly=True, required=True) logs = fields.One2Many('cash_bank.transfer.log_action', 'resource', 'Logs', readonly=True) @classmethod def __register__(cls, module_name): super(Transfer, cls).__register__(module_name) table = cls.__table_handler__(module_name) if table.column_exist('party'): table.drop_column('party') @classmethod def __setup__(cls): super(Transfer, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._transitions |= set(( ('draft', 'confirmed'), ('confirmed', 'posted'), ('confirmed', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['confirmed']), }, 'confirm': { 'invisible': ~Eval('state').in_(['draft']), }, 'post': { 'invisible': ~Eval('state').in_(['confirmed']), }, 'draft': { 'invisible': ~Eval('state').in_(['cancel']), 'icon': If( Eval('state') == 'cancel', 'tryton-clear', 'tryton-go-previous'), }, }) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_cash(): return Decimal('0.0') @staticmethod def default_total_documents(): return Decimal('0.0') @staticmethod def default_total(): return Decimal('0.0') @staticmethod def default_state(): return 'draft' @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.id @staticmethod def default_currency_digits(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.digits return 2 @fields.depends('currency') def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 @fields.depends('cash_bank_from', 'type_from', 'cash_bank_to', 'type_to') def on_change_company(self): self.cash_bank_from = None self.type_from = None self.cash_bank_to = None self.type_to = None @fields.depends('type_from', 'type_to', 'cash_bank_to') def on_change_cash_bank_from(self): self.type_from = None self.cash_bank_to = None self.type_to = None @fields.depends('type_to') def on_change_cash_bank_to(self): self.type_to = None @fields.depends('type_from', 'cash_bank_from') def on_change_type_from(self): if self.type_from: self.cash_bank_from = self.type_from.cash_bank @fields.depends('type_to', 'cash_bank_to') def on_change_type_to(self): if self.type_to: self.cash_bank_to = self.type_to.cash_bank @fields.depends('cash', 'total_documents') def on_change_cash(self): if not self.cash: self.cash = Decimal('0.0') self._set_total() @fields.depends('documents', 'cash', 'total_documents') def on_change_documents(self): self.total_documents = self.get_total_documents() self._set_total() def get_total_documents(self, name=None): total = Decimal('0.0') if self.documents: for doc in self.documents: if doc.amount: total += doc.amount return total def get_total(self, name=None): total = self.cash + self.total_documents return total def _set_total(self): self.total = Decimal('0.0') if self.cash: self.total += self.cash if self.total_documents: self.total += self.total_documents def _new_receipt(self, cash_bank, type_): Receipt = Pool().get('cash_bank.receipt') party = None if type_.party_required: party = self.company.party return Receipt(company=self.company, currency=self.currency, date=self.date, cash_bank=cash_bank, type=type_, party=party, reference=self.reference, description=self.description, cash=self.cash) def _create_line(self, type_, amount, transfer_account): Line = Pool().get('cash_bank.receipt.line') line = Line() line.description = 'Transfer' line.account = transfer_account line.amount = amount return line def _get_doc(self, receipt, docs): Docs = Pool().get('cash_bank.document') for doc in docs: doc.last_receipt = receipt Docs.save(docs) return docs def _create_receipt(self, cash_bank, type_, trasnfer_account, docs): receipt = self._new_receipt(cash_bank, type_) receipt.documents = self._get_doc(receipt, docs) receipt.save() receipt.lines = [ self._create_line(type_, receipt.total, trasnfer_account), ] receipt.save() return receipt def create_receipts(self): pool = Pool() Config = pool.get('cash_bank.configuration') Receipt = pool.get('cash_bank.receipt') config = Config(1) transfer_account = config.account_transfer receipt_from = self._create_receipt(self.cash_bank_from, self.type_from, transfer_account, self.documents) Receipt.confirm([receipt_from]) receipt_to = self._create_receipt(self.cash_bank_to, self.type_to, transfer_account, self.documents) Receipt.confirm([receipt_to]) self.receipt_from = receipt_from self.receipt_to = receipt_to @classmethod def set_transfer(cls, receipts, transfer): for receipt in receipts: receipt.transfer = transfer receipt.save() @classmethod def create(cls, vlist): transfers = super(Transfer, cls).create(vlist) write_log('Created', transfers) return transfers @classmethod def delete(cls, transfers): for transfer in transfers: if transfer.state != 'draft': raise UserError( gettext('cash_bank.msg_delete_document_cash_bank', doc_name='Transfer', doc_number=transfer.rec_name, state='Draft')) super(Transfer, cls).delete(transfers) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, transfers): Receipt = Pool().get('cash_bank.receipt') for transfer in transfers: receipt_from = transfer.receipt_from receipt_to = transfer.receipt_to transfer.receipt_from = None transfer.receipt_to = None Receipt.draft([receipt_from, receipt_to]) Receipt.delete([receipt_to]) Receipt.delete([receipt_from]) cls.save(transfers) write_log('Draft', transfers) @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, transfers): for transfer in transfers: if transfer.total <= 0: raise UserError(gettext('cash_bank.msg_no_total_cash_bank')) transfer.create_receipts() cls.set_transfer([transfer.receipt_from, transfer.receipt_to], transfer) cls.save(transfers) # Update receipts values write_log('Confirmed', transfers) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, transfers): Receipt = Pool().get('cash_bank.receipt') rcps = [] for transfer in transfers: rcps += [transfer.receipt_from, transfer.receipt_to] Receipt.post(rcps) write_log('Posted', transfers) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, transfers): Receipt = Pool().get('cash_bank.receipt') rcps = [] for transfer in transfers: rcps += [transfer.receipt_from, transfer.receipt_to] Receipt.cancel(rcps) write_log('Cancelled', transfers)
class ModelSQLRequiredField(ModelSQL): 'model with a required field' __name__ = 'test.modelsql' integer = fields.Integer(string="integer", required=True) desc = fields.Char(string="desc", required=True)
class Convertion(Workflow, ModelSQL, ModelView): "Cash/Bank Convert Documents to Cash" __name__ = "cash_bank.convertion" company = fields.Many2One( 'company.company', 'Company', required=True, states={ 'readonly': True, }, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], select=True) number = fields.Char('Number', size=None, readonly=True, select=True) cash_bank = fields.Many2One('cash_bank.cash_bank', 'Cash', required=True, domain=[ ('company', '=', Eval('company')), ('type', '=', 'cash'), ], states={ 'readonly': Or( Eval('state') != 'draft', Eval('documents')), }, depends=_DEPENDS + ['company', 'documents']) date = fields.Date('Date', required=True, states=_STATES, depends=_DEPENDS) description = fields.Char('Description', size=None) currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': True, }, depends=['state', 'documents']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') documents = fields.Many2Many( 'cash_bank.document-cash_bank.convertion', 'convertion', 'document', 'Documents', domain=[('last_receipt', '!=', None), ('last_receipt.cash_bank.id', '=', Eval('cash_bank')), If( Eval('state') == 'draft', [ ('convertion', '=', None), ], [ ('convertion', '!=', None), ('convertion.id', '=', Eval('id')), ])], states=_STATES, depends=_DEPENDS + ['cash_bank', 'id']) total_documents = fields.Function( fields.Numeric('Total Documents', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total_documents') state = fields.Selection(STATES, 'State', readonly=True, required=True) logs = fields.One2Many('cash_bank.convertion.log_action', 'resource', 'Logs', readonly=True) @classmethod def __setup__(cls): super(Convertion, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._transitions |= set(( ('draft', 'confirmed'), ('confirmed', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['confirmed']), }, 'confirm': { 'invisible': ~Eval('state').in_(['draft']), }, 'draft': { 'invisible': ~Eval('state').in_(['cancel']), 'icon': If( Eval('state') == 'cancel', 'tryton-clear', 'tryton-go-previous'), }, }) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_total_documents(): return Decimal('0.0') @staticmethod def default_state(): return 'draft' @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.id @staticmethod def default_currency_digits(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.digits return 2 @fields.depends('currency') def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 @fields.depends('documents') def on_change_documents(self): self.total_documents = self.get_total_documents() def get_total_documents(self, name=None): total = Decimal('0.0') if self.documents: for doc in self.documents: if doc.amount: total += doc.amount return total def get_rec_name(self, name): if self.number: return self.number return str(self.id) @classmethod def search_rec_name(cls, name, clause): return [('number', ) + tuple(clause[1:])] @classmethod def set_number(cls, convertions): pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('cash_bank.configuration') config = Config(1) for convertion in convertions: if convertion.number: continue convertion.number = Sequence.get_id(config.convertion_seq.id) cls.save(convertions) @classmethod def create(cls, vlist): convertions = super(Convertion, cls).create(vlist) write_log('Created', convertions) return convertions @classmethod def delete(cls, convertions): Document = Pool().get('cash_bank.document') docs = [] for convertion in convertions: if convertion.state != 'draft': raise UserError( gettext('cash_bank.msg_delete_document_cash_bank', doc_name='Convertion', doc_number=convertion.rec_name, state='Draft')) for doc in convertion.documents: doc.convertion = None docs.append(doc) write_log('Convertion ' + convertion.rec_name + ' deleted.', [doc]) Document.save(docs) super(Convertion, cls).delete(convertions) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, convertions): Document = Pool().get('cash_bank.document') docs = [] for convertion in convertions: for doc in convertion.documents: doc.convertion = None docs.append(doc) write_log('Convertion ' + convertion.rec_name + ' to Draft.', [doc]) Document.save(docs) write_log('Draft', convertions) @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, convertions): Document = Pool().get('cash_bank.document') docs = [] for convertion in convertions: for doc in convertion.documents: doc.convertion = convertion write_log('Convertion ' + convertion.rec_name + ' confirmed.', [doc]) docs.append(doc) Document.save(docs) cls.set_number(convertions) write_log('Confirmed', convertions) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, convertions): write_log('Cancelled', convertions)
class IntegerRequired(ModelSQL): 'Integer Required' __name__ = 'test.integer_required' integer = fields.Integer(string='Integer', help='Test integer', required=True)
class IntegerDomain(ModelSQL): 'Integer Domain' __name__ = 'test.integer_domain' integer = fields.Integer('Integer', domain=[('integer', '>', 42)])
class Statement(Workflow, ModelSQL, ModelView): 'Account Statement' __name__ = 'account.statement' name = fields.Char('Name', required=True) company = fields.Many2One( 'company.company', 'Company', required=True, select=True, states=_STATES, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=_DEPENDS) journal = fields.Many2One('account.statement.journal', 'Journal', required=True, select=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'readonly': (Eval('state') != 'draft') | Eval('lines', [0]), }, depends=['state', 'company']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') date = fields.Date('Date', required=True, select=True) start_balance = fields.Numeric('Start Balance', digits=(16, Eval('currency_digits', 2)), states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits']) end_balance = fields.Numeric('End Balance', digits=(16, Eval('currency_digits', 2)), states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits']) balance = fields.Function( fields.Numeric('Balance', digits=(16, Eval('currency_digits', 2)), states=_BALANCE_STATES, depends=_BALANCE_DEPENDS + ['currency_digits']), 'on_change_with_balance') total_amount = fields.Numeric('Total Amount', digits=(16, Eval('currency_digits', 2)), states=_AMOUNT_STATES, depends=_AMOUNT_DEPENDS + ['currency_digits']) number_of_lines = fields.Integer('Number of Lines', states=_NUMBER_STATES, depends=_NUMBER_DEPENDS) lines = fields.One2Many('account.statement.line', 'statement', 'Lines', states={ 'readonly': (Eval('state') != 'draft') | ~Eval('journal'), }, depends=['state', 'journal']) origins = fields.One2Many('account.statement.origin', 'statement', "Origins", states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) origin_file = fields.Binary("Origin File", readonly=True, file_id=file_id, store_prefix=store_prefix) origin_file_id = fields.Char("Origin File ID", readonly=True) state = fields.Selection(STATES, 'State', readonly=True, select=True) validation = fields.Function(fields.Char('Validation'), 'on_change_with_validation') @classmethod def __setup__(cls): super(Statement, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._error_messages.update({ 'wrong_end_balance': 'End Balance must be "%s".', 'wrong_total_amount': 'Total Amount must be "%s".', 'wrong_number_of_lines': 'Number of Lines must be "%s".', 'delete_cancel': ('Statement "%s" must be cancelled before ' 'deletion.'), 'paid_invoice_draft_statement': ('There are paid invoices on ' 'draft statements.'), 'debit_credit_account_statement_journal': ('Please provide ' 'debit and credit account on statement journal "%s".'), 'post_with_pending_amount': ('Origin line "%(origin)s" ' 'of statement "%(statement)s" still has a pending amount ' 'of "%(amount)s".'), }) cls._transitions |= set(( ('draft', 'validated'), ('draft', 'cancel'), ('validated', 'posted'), ('validated', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') != 'cancel', }, 'validate_statement': { 'invisible': Eval('state') != 'draft', }, 'post': { 'invisible': Eval('state') != 'validated', }, 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'validated']), }, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') transaction = Transaction() cursor = transaction.connection.cursor() sql_table = cls.__table__() # Migration from 1.8: new field company table = TableHandler(cls, module_name) company_exist = table.column_exist('company') super(Statement, cls).__register__(module_name) # Migration from 1.8: fill new field company if not company_exist: offset = 0 limit = transaction.database.IN_MAX statements = True while statements: statements = cls.search([], offset=offset, limit=limit) offset += limit for statement in statements: cls.write([statement], { 'company': statement.journal.company.id, }) table = TableHandler(cls, module_name) table.not_null_action('company', action='add') # Migration from 3.2: remove required on start/end balance table.not_null_action('start_balance', action='remove') table.not_null_action('end_balance', action='remove') # Migration from 3.2: add required name cursor.execute(*sql_table.update( [sql_table.name], [sql_table.id.cast(cls.name.sql_type().base)], where=sql_table.name == Null)) @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_currency_digits(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.digits return 2 @fields.depends('journal', 'state', 'lines') def on_change_journal(self): if not self.journal: return statements = self.search([ ('journal', '=', self.journal.id), ], order=[ ('date', 'DESC'), ('id', 'DESC'), ], limit=1) if not statements: return statement, = statements self.start_balance = statement.end_balance @fields.depends('journal') def on_change_with_currency_digits(self, name=None): if self.journal: return self.journal.currency.digits return 2 def get_end_balance(self, name): end_balance = self.start_balance for line in self.lines: end_balance += line.amount return end_balance @fields.depends('start_balance', 'end_balance') def on_change_with_balance(self, name=None): return ((getattr(self, 'end_balance', 0) or 0) - (getattr(self, 'start_balance', 0) or 0)) @fields.depends('lines', 'journal') def on_change_lines(self): pool = Pool() Currency = pool.get('currency.currency') Line = pool.get('account.statement.line') if self.journal and self.lines: invoices = set() for line in self.lines: if getattr(line, 'invoice', None): invoices.add(line.invoice) invoice_id2amount_to_pay = {} for invoice in invoices: with Transaction().set_context(date=invoice.currency_date): if invoice.type == 'out': sign = -1 else: sign = 1 invoice_id2amount_to_pay[ invoice.id] = sign * (Currency.compute( invoice.currency, invoice.amount_to_pay, self.journal.currency)) lines = list(self.lines) line_offset = 0 for index, line in enumerate(self.lines or []): if getattr(line, 'invoice', None) and line.id: amount_to_pay = invoice_id2amount_to_pay[line.invoice.id] if (not self.journal.currency.is_zero(amount_to_pay) and getattr(line, 'amount', None) and (line.amount >= 0) == (amount_to_pay <= 0)): if abs(line.amount) > abs(amount_to_pay): new_line = Line() for field_name, field in Line._fields.iteritems(): if field_name == 'id': continue try: setattr(new_line, field_name, getattr(line, field_name)) except AttributeError: pass new_line.amount = line.amount + amount_to_pay new_line.invoice = None line_offset += 1 lines.insert(index + line_offset, new_line) invoice_id2amount_to_pay[line.invoice.id] = 0 line.amount = amount_to_pay.copy_sign(line.amount) else: invoice_id2amount_to_pay[line.invoice.id] = ( line.amount + amount_to_pay) else: line.invoice = None self.lines = lines @fields.depends('journal') def on_change_with_validation(self, name=None): if self.journal: return self.journal.validation def _group_key(self, line): key = ( ('number', line.number or Unequal()), ('date', line.date), ('party', line.party), ) return key def _get_grouped_line(self): "Return Line class for grouped lines" assert self.lines keys = [k[0] for k in self._group_key(self.lines[0])] class Line(namedtuple('Line', keys + ['lines'])): @property def amount(self): return sum((l.amount for l in self.lines)) @property def descriptions(self): done = set() for line in self.lines: if line.description and line.description not in done: done.add(line.description) yield line.description return Line @property def grouped_lines(self): if self.origins: for origin in self.origins: yield origin elif self.lines: Line = self._get_grouped_line() for key, lines in groupby(self.lines, key=self._group_key): yield Line(**dict(key + (('lines', list(lines)), ))) @classmethod def delete(cls, statements): # Cancel before delete cls.cancel(statements) for statement in statements: if statement.state != 'cancel': cls.raise_user_error('delete_cancel', (statement.rec_name, )) super(Statement, cls).delete(statements) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, statements): pass def validate_balance(self): pool = Pool() Lang = pool.get('ir.lang') end_balance = (self.start_balance + sum(l.amount for l in self.lines)) if end_balance != self.end_balance: lang, = Lang.search([ ('code', '=', Transaction().language), ]) amount = Lang.format( lang, '%.' + str(self.journal.currency.digits) + 'f', end_balance, True) self.raise_user_error('wrong_end_balance', amount) def validate_amount(self): pool = Pool() Lang = pool.get('ir.lang') amount = sum(l.amount for l in self.lines) if amount != self.total_amount: lang, = Lang.search([ ('code', '=', Transaction().language), ]) amount = Lang.format( lang, '%.' + str(self.journal.currency.digits) + 'f', amount, True) self.raise_user_error('wrong_total_amount', amount) def validate_number_of_lines(self): number = len(list(self.grouped_lines)) if number != self.number_of_lines: self.raise_user_error('wrong_number_of_lines', number) @classmethod @ModelView.button @Workflow.transition('validated') def validate_statement(cls, statements): pool = Pool() Line = pool.get('account.statement.line') for statement in statements: getattr(statement, 'validate_%s' % statement.validation)() cls.create_move(statements) cls.write(statements, { 'state': 'validated', }) common_lines = Line.search([ ('statement.state', '=', 'draft'), ('invoice.state', '=', 'paid'), ]) if common_lines: warning_key = '_'.join(str(l.id) for l in common_lines) cls.raise_user_warning(warning_key, 'paid_invoice_draft_statement') Line.write(common_lines, { 'invoice': None, }) @classmethod def create_move(cls, statements): '''Create move for the statements and try to reconcile the lines. Returns the list of move, statement and lines ''' pool = Pool() Line = pool.get('account.statement.line') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') moves = [] for statement in statements: for key, lines in groupby(statement.lines, key=statement._group_key): lines = list(lines) key = dict(key) move = statement._get_move(key) moves.append((move, statement, lines)) Move.save([m for m, _, _ in moves]) to_write = [] for move, _, lines in moves: to_write.append(lines) to_write.append({ 'move': move.id, }) if to_write: Line.write(*to_write) move_lines = [] for move, statement, lines in moves: amount = 0 amount_second_currency = 0 for line in lines: move_line = line.get_move_line() move_line.move = move amount += move_line.debit - move_line.credit if move_line.amount_second_currency: amount_second_currency += move_line.amount_second_currency move_lines.append((move_line, line)) move_line = statement._get_move_line(amount, amount_second_currency, lines) move_line.move = move move_lines.append((move_line, None)) MoveLine.save([l for l, _ in move_lines]) for move_line, line in move_lines: if line: line.reconcile(move_line) return moves def _get_move(self, key): 'Return Move for the grouping key' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') period_id = Period.find(self.company.id, date=key['date']) return Move( period=period_id, journal=self.journal.journal, date=key['date'], origin=self, company=self.company, description=unicode(key['number']), ) def _get_move_line(self, amount, amount_second_currency, lines): 'Return counterpart Move Line for the amount' pool = Pool() MoveLine = pool.get('account.move.line') if amount < 0: account = self.journal.journal.debit_account else: account = self.journal.journal.credit_account if not account: self.raise_user_error('debit_credit_account_statement_journal', (self.journal.rec_name, )) if self.journal.currency != self.company.currency: second_currency = self.journal.currency amount_second_currency *= -1 else: second_currency = None amount_second_currency = None descriptions = {l.description for l in lines} if len(descriptions) == 1: description, = descriptions else: description = '' return MoveLine( debit=abs(amount) if amount < 0 else 0, credit=abs(amount) if amount > 0 else 0, account=account, second_currency=second_currency, amount_second_currency=amount_second_currency, description=description, ) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, statements): StatementLine = Pool().get('account.statement.line') for statement in statements: for origin in statement.origins: if origin.pending_amount: cls.raise_user_error( 'post_with_pending_amount', { 'origin': origin.rec_name, 'amount': origin.pending_amount, 'statement': statement.rec_name, }) lines = [l for s in statements for l in s.lines] StatementLine.post_move(lines) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, statements): StatementLine = Pool().get('account.statement.line') lines = [l for s in statements for l in s.lines] StatementLine.delete_move(lines)
class LineGroup(ModelSQL, ModelView): 'Account Statement Line Group' __name__ = 'account.statement.line.group' _rec_name = 'number' statement = fields.Many2One('account.statement', 'Statement') journal = fields.Function(fields.Many2One('account.statement.journal', 'Journal'), 'get_journal', searcher='search_journal') number = fields.Char('Number') date = fields.Date('Date') amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'get_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') party = fields.Many2One('party.party', 'Party') move = fields.Many2One('account.move', 'Move') @classmethod def __setup__(cls): super(LineGroup, cls).__setup__() cls._order.insert(0, ('date', 'DESC')) @classmethod def _grouped_columns(cls, line): return [ Max(line.statement).as_('statement'), Max(line.number).as_('number'), Max(line.date).as_('date'), Sum(line.amount).as_('amount'), Max(line.party).as_('party'), ] @classmethod def table_query(cls): pool = Pool() Move = pool.get('account.move') Line = pool.get('account.statement.line') move = Move.__table__() line = Line.__table__() std_columns = [ move.id, move.create_uid, move.create_date, move.write_uid, move.write_date, ] columns = (std_columns + [move.id.as_('move')] + cls._grouped_columns(line)) return move.join(line, condition=move.id == line.move).select( *columns, where=move.origin.ilike(Statement.__name__ + ',%'), group_by=std_columns + [move.id]) def get_journal(self, name): return self.statement.journal.id @classmethod def search_journal(cls, name, clause): return [('statement.' + clause[0], ) + tuple(clause[1:])] def get_currency(self, name): return self.statement.journal.currency.id def get_currency_digits(self, name): return self.statement.journal.currency.digits
class Uom(ModelSQL, ModelView): 'Unit of measure' __name__ = 'product.uom' name = fields.Char('Name', size=None, required=True, states=STATES, translate=True, depends=DEPENDS) symbol = fields.Char('Symbol', size=10, required=True, states=STATES, translate=True, depends=DEPENDS) category = fields.Many2One('product.uom.category', 'Category', required=True, ondelete='RESTRICT', states=STATES, depends=DEPENDS) rate = fields.Float('Rate', digits=(12, 12), required=True, states=STATES, depends=DEPENDS, help=('The coefficient for the formula:\n' '1 (base unit) = coef (this unit)')) factor = fields.Float('Factor', digits=(12, 12), states=STATES, required=True, depends=DEPENDS, help=('The coefficient for the formula:\n' 'coef (base unit) = 1 (this unit)')) rounding = fields.Float('Rounding Precision', digits=(12, 12), required=True, states=STATES, depends=DEPENDS, domain=[ ('rounding', '>', 0), ]) digits = fields.Integer('Display Digits', required=True) active = fields.Boolean('Active') @classmethod def __register__(cls, module_name): cursor = Transaction().cursor model_data = Table('ir_model_data') # Migration from 1.6: corrected misspelling of ounce (was once) cursor.execute( *model_data.update(columns=[model_data.fs_id], values=['uom_ounce'], where=(model_data.fs_id == 'uom_once') & (model_data.module == 'product'))) super(Uom, cls).__register__(module_name) @classmethod def __setup__(cls): super(Uom, cls).__setup__() t = cls.__table__() cls._sql_constraints += [ ('non_zero_rate_factor', Check(t, (t.rate != 0) | (t.factor != 0)), 'Rate and factor can not be both equal to zero.') ] cls._order.insert(0, ('name', 'ASC')) cls._error_messages.update({ 'change_uom_rate_title': ('You cannot change Rate, Factor or ' 'Category on a Unit of Measure. '), 'change_uom_rate': ('If the UOM is still not used, you can ' 'delete it otherwise you can deactivate it ' 'and create a new one.'), 'invalid_factor_and_rate': ('Invalid Factor and Rate values in UOM "%s".'), }) @classmethod def check_xml_record(cls, records, values): return True @staticmethod def default_rate(): return 1.0 @staticmethod def default_factor(): return 1.0 @staticmethod def default_active(): return True @staticmethod def default_rounding(): return 0.01 @staticmethod def default_digits(): return 2 @fields.depends('factor') def on_change_factor(self): if (self.factor or 0.0) == 0.0: self.rate = 0.0 else: self.rate = round(1.0 / self.factor, self.__class__.rate.digits[1]) @fields.depends('rate') def on_change_rate(self): if (self.rate or 0.0) == 0.0: self.factor = 0.0 else: self.factor = round(1.0 / self.rate, self.__class__.factor.digits[1]) @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, (cls._rec_name, ) + tuple(clause[1:]), ('symbol', ) + tuple(clause[1:]), ] @staticmethod def round(number, precision=1.0): i, d = divmod(precision, 1) base = round(number / precision) # Instead of multiply by the decimal part, we must divide by the # integer to avoid precision lost due to float return (base * i) + ((base / (1 / d)) if d != 0 else 0) @classmethod def validate(cls, uoms): super(Uom, cls).validate(uoms) for uom in uoms: uom.check_factor_and_rate() def check_factor_and_rate(self): "Check coherence between factor and rate" if self.rate == self.factor == 0.0: return if (self.rate != round(1.0 / self.factor, self.__class__.rate.digits[1]) and self.factor != round(1.0 / self.rate, self.__class__.factor.digits[1])): self.raise_user_error('invalid_factor_and_rate', (self.rec_name, )) @classmethod def write(cls, *args): if Transaction().user == 0: super(Uom, cls).write(*args) return actions = iter(args) all_uoms = [] for uoms, values in zip(actions, actions): if 'rate' not in values and 'factor' not in values \ and 'category' not in values: continue all_uoms += uoms old_uom = dict((uom.id, (uom.factor, uom.rate, uom.category.id)) for uom in all_uoms) super(Uom, cls).write(*args) for uom in all_uoms: if uom.factor != old_uom[uom.id][0] \ or uom.rate != old_uom[uom.id][1] \ or uom.category.id != old_uom[uom.id][2]: cls.raise_user_error('change_uom_rate_title', error_description='change_uom_rate') @property def accurate_field(self): """ Select the more accurate field. It chooses the field that has the least decimal. """ lengths = {} for field in ('rate', 'factor'): format = '%%.%df' % getattr(self.__class__, field).digits[1] lengths[field] = len( (format % getattr(self, field)).split('.')[1].rstrip('0')) if lengths['rate'] < lengths['factor']: return 'rate' elif lengths['factor'] < lengths['rate']: return 'factor' elif self.factor >= 1.0: return 'factor' else: return 'rate' @classmethod def compute_qty(cls, from_uom, qty, to_uom, round=True): """ Convert quantity for given uom's. """ if not qty or (from_uom is None and to_uom is None): return qty if from_uom is None: raise ValueError("missing from_uom") if to_uom is None: raise ValueError("missing to_uom") if from_uom.category.id != to_uom.category.id: raise ValueError("cannot convert between %s and %s" % (from_uom.category.name, to_uom.category.name)) if from_uom.accurate_field == 'factor': amount = qty * from_uom.factor else: amount = qty / from_uom.rate if to_uom.accurate_field == 'factor': amount = amount / to_uom.factor else: amount = amount * to_uom.rate if round: amount = cls.round(amount, to_uom.rounding) return amount @classmethod def compute_price(cls, from_uom, price, to_uom): """ Convert price for given uom's. """ if not price or (from_uom is None and to_uom is None): return price if from_uom is None: raise ValueError("missing from_uom") if to_uom is None: raise ValueError("missing to_uom") if from_uom.category.id != to_uom.category.id: raise ValueError('cannot convert between %s and %s' % (from_uom.category.name, to_uom.category.name)) factor_format = '%%.%df' % cls.factor.digits[1] rate_format = '%%.%df' % cls.rate.digits[1] if from_uom.accurate_field == 'factor': new_price = price / Decimal(factor_format % from_uom.factor) else: new_price = price * Decimal(rate_format % from_uom.rate) if to_uom.accurate_field == 'factor': new_price = new_price * Decimal(factor_format % to_uom.factor) else: new_price = new_price / Decimal(rate_format % to_uom.rate) return new_price
class Lang(DeactivableMixin, ModelSQL, ModelView): "Language" __name__ = "ir.lang" name = fields.Char('Name', required=True, translate=True) code = fields.Char('Code', required=True, help="RFC 4646 tag: http://tools.ietf.org/html/rfc4646") translatable = fields.Boolean('Translatable', readonly=True) parent = fields.Char("Parent Code", help="Code of the exceptional parent") direction = fields.Selection([ ('ltr', 'Left-to-right'), ('rtl', 'Right-to-left'), ], 'Direction', required=True) # date date = fields.Char('Date', required=True) am = fields.Char("AM") pm = fields.Char("PM") # number grouping = fields.Char('Grouping', required=True) decimal_point = fields.Char('Decimal Separator', required=True) thousands_sep = fields.Char('Thousands Separator') # monetary formatting mon_grouping = fields.Char('Grouping', required=True) mon_decimal_point = fields.Char('Decimal Separator', required=True) mon_thousands_sep = fields.Char('Thousands Separator') p_sign_posn = fields.Integer('Positive Sign Position', required=True) n_sign_posn = fields.Integer('Negative Sign Position', required=True) positive_sign = fields.Char('Positive Sign') negative_sign = fields.Char('Negative Sign') p_cs_precedes = fields.Boolean('Positive Currency Symbol Precedes') n_cs_precedes = fields.Boolean('Negative Currency Symbol Precedes') p_sep_by_space = fields.Boolean('Positive Separate by Space') n_sep_by_space = fields.Boolean('Negative Separate by Space') _lang_cache = Cache('ir.lang') _code_cache = Cache('ir.lang.code', context=False) @classmethod def __setup__(cls): super(Lang, cls).__setup__() table = cls.__table__() cls._sql_constraints += [ ('check_decimal_point_thousands_sep', Check(table, table.decimal_point != table.thousands_sep), 'decimal_point and thousands_sep must be different!'), ] cls._buttons.update({ 'load_translations': {}, 'unload_translations': { 'invisible': ~Eval('translatable', False), }, }) @classmethod def search_rec_name(cls, name, clause): langs = cls.search([('code', ) + tuple(clause[1:])], order=[]) if langs: langs += cls.search([('name', ) + tuple(clause[1:])], order=[]) return [('id', 'in', [l.id for l in langs])] return [('name', ) + tuple(clause[1:])] @classmethod def read(cls, ids, fields_names): pool = Pool() Translation = pool.get('ir.translation') Config = pool.get('ir.configuration') res = super(Lang, cls).read(ids, fields_names) if (Transaction().context.get('translate_name') and (not fields_names or 'name' in fields_names)): with Transaction().set_context(language=Config.get_language(), translate_name=False): res2 = cls.read(ids, ['id', 'code', 'name']) for record2 in res2: for record in res: if record['id'] == record2['id']: break res_trans = Translation.get_ids(cls.__name__ + ',name', 'model', record2['code'], [record2['id']]) record['name'] = (res_trans.get(record2['id'], False) or record2['name']) return res @staticmethod def default_translatable(): return False @staticmethod def default_direction(): return 'ltr' @staticmethod def default_date(): return '%m/%d/%Y' @staticmethod def default_grouping(): return '[]' @staticmethod def default_decimal_point(): return '.' @staticmethod def default_thousands_sep(): return ',' @classmethod def default_mon_grouping(cls): return '[]' @classmethod def default_mon_thousands_sep(cls): return ',' @classmethod def default_mon_decimal_point(cls): return '.' @classmethod def default_p_sign_posn(cls): return 1 @classmethod def default_n_sign_posn(cls): return 1 @classmethod def default_negative_sign(cls): return '-' @classmethod def default_positive_sign(cls): return '' @classmethod def default_p_cs_precedes(cls): return True @classmethod def default_n_cs_precedes(cls): return True @classmethod def default_p_sep_by_space(cls): return False @classmethod def default_n_sep_by_space(cls): return False @classmethod @ModelView.button def load_translations(cls, languages): pool = Pool() Module = pool.get('ir.module') codes = set() cls.write(languages, {'translatable': True}) for language in languages: code = language.code while code: codes.add(code) code = get_parent_language(code) modules = Module.search([ ('state', '=', 'activated'), ]) modules = [m.name for m in modules] for node in create_graph(modules): load_translations(pool, node, codes) @classmethod @ModelView.button def unload_translations(cls, languages): pool = Pool() Translation = pool.get('ir.translation') cls.write(languages, {'translatable': False}) languages = [l.code for l in languages] Translation.delete( Translation.search([ ('lang', 'in', languages), ('module', '!=', None), ])) @classmethod def validate(cls, languages): super(Lang, cls).validate(languages) cls.check_grouping(languages) cls.check_date(languages) cls.check_translatable(languages) @classmethod def check_grouping(cls, langs): ''' Check if grouping is list of numbers ''' for lang in langs: for grouping in [lang.grouping, lang.mon_grouping]: try: grouping = literal_eval(grouping) for i in grouping: if not isinstance(i, int): raise except Exception: raise GroupingError( gettext('ir.msg_language_invalid_grouping', grouping=grouping, language=lang.rec_name)) @classmethod def check_date(cls, langs): ''' Check the date format ''' for lang in langs: date = lang.date try: datetime.datetime.now().strftime(date) except Exception: raise DateError( gettext('ir.msg_language_invalid_date', format=lang.date, language=lang.rec_name)) if (('%Y' not in lang.date) or ('%b' not in lang.date and '%B' not in lang.date and '%m' not in lang.date and '%-m' not in lang.date) or ('%d' not in lang.date and '%-d' not in lang.date and '%j' not in lang.date and '%-j' not in lang.date) or ('%x' in lang.date or '%X' in lang.date or '%c' in lang.date or '%Z' in lang.date)): raise DateError( gettext('ir.msg_language_invalid_date', format=lang.date, language=lang.rec_name)) @classmethod def check_translatable(cls, langs): pool = Pool() Config = pool.get('ir.configuration') # Skip check for root because when languages are created from XML file, # translatable is not yet set. if Transaction().user == 0: return True for lang in langs: if (lang.code == Config.get_language() and not lang.translatable): raise TranslatableError( gettext('ir.msg_language_default_translatable')) @staticmethod def check_xml_record(langs, values): return True @classmethod def get_translatable_languages(cls): res = cls._lang_cache.get('translatable_languages') if res is None: langs = cls.search([ ('translatable', '=', True), ]) res = [x.code for x in langs] cls._lang_cache.set('translatable_languages', res) return res @classmethod def create(cls, vlist): pool = Pool() Translation = pool.get('ir.translation') # Clear cache cls._lang_cache.clear() languages = super(Lang, cls).create(vlist) Translation._get_language_cache.clear() _parents.clear() return languages @classmethod def write(cls, langs, values, *args): pool = Pool() Translation = pool.get('ir.translation') # Clear cache cls._lang_cache.clear() cls._code_cache.clear() super(Lang, cls).write(langs, values, *args) Translation._get_language_cache.clear() _parents.clear() @classmethod def delete(cls, langs): pool = Pool() Config = pool.get('ir.configuration') Translation = pool.get('ir.translation') for lang in langs: if lang.code == Config.get_language(): raise DeleteDefaultError( gettext('ir.msg_language_delete_default')) # Clear cache cls._lang_cache.clear() cls._code_cache.clear() super(Lang, cls).delete(langs) Translation._get_language_cache.clear() _parents.clear() @classmethod def get(cls, code=None): "Return language instance for the code or the transaction language" if code is None: code = Transaction().language lang_id = cls._code_cache.get(code) if not lang_id: with Transaction().set_context(active_test=False): lang, = cls.search([ ('code', '=', code), ]) cls._code_cache.set(code, lang.id) else: lang = cls(lang_id) return lang def _group(self, s, monetary=False): # Code from _group in locale.py # Iterate over grouping intervals def _grouping_intervals(grouping): last_interval = 0 for interval in grouping: # if grouping is -1, we are done if interval == CHAR_MAX: return # 0: re-use last group ad infinitum if interval == 0: while True: yield last_interval yield interval last_interval = interval if monetary: thousands_sep = self.mon_thousands_sep grouping = literal_eval(self.mon_grouping) else: thousands_sep = self.thousands_sep grouping = literal_eval(self.grouping) if not grouping: return (s, 0) if s[-1] == ' ': stripped = s.rstrip() right_spaces = s[len(stripped):] s = stripped else: right_spaces = '' left_spaces = '' groups = [] for interval in _grouping_intervals(grouping): if not s or s[-1] not in "0123456789": # only non-digit characters remain (sign, spaces) left_spaces = s s = '' break groups.append(s[-interval:]) s = s[:-interval] if s: groups.append(s) groups.reverse() return (left_spaces + thousands_sep.join(groups) + right_spaces, len(thousands_sep) * (len(groups) - 1)) def format(self, percent, value, grouping=False, monetary=False, *additional): ''' Returns the lang-aware substitution of a %? specifier (percent). ''' # Code from format in locale.py # Strip a given amount of excess padding from the given string def _strip_padding(s, amount): lpos = 0 while amount and s[lpos] == ' ': lpos += 1 amount -= 1 rpos = len(s) - 1 while amount and s[rpos] == ' ': rpos -= 1 amount -= 1 return s[lpos:rpos + 1] # this is only for one-percent-specifier strings # and this should be checked if percent[0] != '%': raise ValueError("format() must be given exactly one %char " "format specifier") if additional: formatted = percent % ((value, ) + additional) else: formatted = percent % value # floats and decimal ints need special action! if percent[-1] in 'eEfFgG': seps = 0 parts = formatted.split('.') if grouping: parts[0], seps = self._group(parts[0], monetary=monetary) if monetary: decimal_point = self.mon_decimal_point else: decimal_point = self.decimal_point formatted = decimal_point.join(parts) if seps: formatted = _strip_padding(formatted, seps) elif percent[-1] in 'diu': seps = 0 if grouping: formatted, seps = self._group(formatted, monetary=monetary) if seps: formatted = _strip_padding(formatted, seps) return formatted def currency(self, val, currency, symbol=True, grouping=False, digits=None): """ Formats val according to the currency settings in lang. """ # Code from currency in locale.py # check for illegal values if digits is None: digits = currency.digits if digits == 127: raise ValueError("Currency formatting is not possible using " "the 'C' locale.") s = self.format('%%.%if' % digits, abs(val), grouping, monetary=True) # '<' and '>' are markers if the sign must be inserted # between symbol and value s = '<' + s + '>' if symbol: smb = currency.symbol precedes = (val < 0 and self.n_cs_precedes or self.p_cs_precedes) separated = (val < 0 and self.n_sep_by_space or self.p_sep_by_space) if precedes: s = smb + (separated and ' ' or '') + s else: s = s + (separated and ' ' or '') + smb sign_pos = val < 0 and self.n_sign_posn or self.p_sign_posn sign = val < 0 and self.negative_sign or self.positive_sign if sign_pos == 0: s = '(' + s + ')' elif sign_pos == 1: s = sign + s elif sign_pos == 2: s = s + sign elif sign_pos == 3: s = s.replace('<', sign) elif sign_pos == 4: s = s.replace('>', sign) else: # the default if nothing specified; # this should be the most fitting sign position s = sign + s return s.replace('<', '').replace('>', '') def strftime(self, value, format=None): ''' Convert value to a string as specified by the format argument. ''' pool = Pool() Month = pool.get('ir.calendar.month') Day = pool.get('ir.calendar.day') if format is None: format = self.date if isinstance(value, datetime.datetime): format += ' %H:%M:%S' format = format.replace('%x', self.date) format = format.replace('%X', '%H:%M:%S') if isinstance(value, datetime.date): for f, i, klass in (('%A', 6, Day), ('%B', 1, Month)): for field, f in [('name', f), ('abbreviation', f.lower())]: locale = klass.locale(self, field=field) format = format.replace(f, locale[value.timetuple()[i]]) if isinstance(value, datetime.time): time = value else: try: time = value.time() except AttributeError: time = None if time: if time < datetime.time(12): p = self.am or 'AM' else: p = self.pm or 'PM' format = format.replace('%p', p) return value.strftime(format)
class Address(ModelSQL, ModelView): "Address" __name__ = 'party.address' party = fields.Many2One('party.party', 'Party', required=True, ondelete='CASCADE', select=True, states={ 'readonly': If(~Eval('active'), True, Eval('id', 0) > 0), }, depends=['active', 'id']) name = fields.Char('Name', states=STATES, depends=DEPENDS) street = fields.Char('Street', states=STATES, depends=DEPENDS) streetbis = fields.Char('Street (bis)', states=STATES, depends=DEPENDS) zip = fields.Char('Zip', states=STATES, depends=DEPENDS) city = fields.Char('City', states=STATES, depends=DEPENDS) country = fields.Many2One('country.country', 'Country', states=STATES, depends=DEPENDS) subdivision = fields.Many2One("country.subdivision", 'Subdivision', domain=[('country', '=', Eval('country'))], states=STATES, depends=['active', 'country']) active = fields.Boolean('Active') sequence = fields.Integer("Sequence") full_address = fields.Function(fields.Text('Full Address'), 'get_full_address') @classmethod def __setup__(cls): super(Address, cls).__setup__() cls._order.insert(0, ('party', 'ASC')) cls._order.insert(1, ('sequence', 'ASC')) cls._error_messages.update({ 'write_party': 'You can not modify the party of address "%s".', }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().cursor table = TableHandler(cursor, cls, module_name) super(Address, cls).__register__(module_name) # Migration from 2.4: drop required on sequence table.not_null_action('sequence', action='remove') @staticmethod def order_sequence(tables): table, _ = tables[None] return [table.sequence == None, table.sequence] @staticmethod def default_active(): return True _autocomplete_limit = 100 def _autocomplete_domain(self): domain = [] if self.country: domain.append(('country', '=', self.country.id)) if self.subdivision: domain.append([ 'OR', ('subdivision', '=', self.subdivision.id), ('subdivision', '=', None), ]) return domain def _autocomplete_search(self, domain, name): pool = Pool() Zip = pool.get('country.zip') if domain: records = Zip.search(domain, limit=self._autocomplete_limit) if len(records) < self._autocomplete_limit: return sorted({getattr(z, name) for z in records}) return [] @fields.depends('city', 'country', 'subdivision') def autocomplete_zip(self): domain = self._autocomplete_domain() if self.city: domain.append(('city', 'ilike', '%%%s%%' % self.city)) return self._autocomplete_search(domain, 'zip') @fields.depends('zip', 'country', 'subdivision') def autocomplete_city(self): domain = self._autocomplete_domain() if self.zip: domain.append(('zip', 'ilike', '%s%%' % self.zip)) return self._autocomplete_search(domain, 'city') def get_full_address(self, name): full_address = '' if self.name: full_address = self.name if self.street: if full_address: full_address += '\n' full_address += self.street if self.streetbis: if full_address: full_address += '\n' full_address += self.streetbis if self.zip or self.city: if full_address: full_address += '\n' if self.zip: full_address += self.zip if self.city: if full_address[-1:] != '\n': full_address += ' ' full_address += self.city if self.country or self.subdivision: if full_address: full_address += '\n' if self.subdivision: full_address += self.subdivision.name if self.country: if full_address[-1:] != '\n': full_address += ' ' full_address += self.country.name return full_address def get_rec_name(self, name): return ", ".join( x for x in [self.name, self.party.rec_name, self.zip, self.city] if x) @classmethod def search_rec_name(cls, name, clause): return [ 'OR', ('zip', ) + tuple(clause[1:]), ('city', ) + tuple(clause[1:]), ('name', ) + tuple(clause[1:]), ('party', ) + tuple(clause[1:]), ] @classmethod def write(cls, *args): actions = iter(args) for addresses, values in zip(actions, actions): if 'party' in values: for address in addresses: if address.party.id != values['party']: cls.raise_user_error('write_party', (address.rec_name, )) super(Address, cls).write(*args) @fields.depends('subdivision', 'country') def on_change_country(self): if (self.subdivision and self.subdivision.country != self.country): return {'subdivision': None} return {}
class Work(metaclass=PoolMeta): __name__ = 'project.work' product = fields.Many2One('product.product', 'Product', domain=[ ('type', '=', 'service'), ('default_uom_category', '=', Id('product', 'uom_cat_time')), ]) list_price = fields.Numeric('List Price', digits=price_digits) revenue = fields.Function(fields.Numeric('Revenue', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total') cost = fields.Function(fields.Numeric('Cost', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') @classmethod def __setup__(cls): pool = Pool() super(Work, cls).__setup__() try: pool.get('purchase.line') except KeyError: pass else: # Add purchase lines if purchase is installed cls.purchase_lines = fields.One2Many('purchase.line', 'work', 'Purchase Lines', domain=[ ('purchase.company', '=', Eval('company', -1)), ('type', '=', 'line'), ], depends=['company']) @classmethod def _get_cost(cls, works): pool = Pool() Line = pool.get('timesheet.line') Work = pool.get('timesheet.work') transaction = Transaction() cursor = transaction.connection.cursor() costs = dict.fromkeys([w.id for w in works], 0) table = cls.__table__() work = Work.__table__() line = Line.__table__() # Timesheet cost work_ids = [w.id for w in works] for sub_ids in grouped_slice(work_ids): red_sql = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(work, condition=( Concat(cls.__name__ + ',', table.id) == work.origin) ).join(line, condition=line.work == work.id ).select(table.id, Sum(line.cost_price * line.duration), where=red_sql, group_by=[table.id])) for work_id, cost in cursor.fetchall(): # SQLite stores timedelta as float if not isinstance(cost, float): cost = cost.total_seconds() # Convert from seconds cost /= 60 * 60 costs[work_id] += Decimal(str(cost)) # Purchase cost if hasattr(cls, 'purchase_lines'): for work_id, cost in cls._purchase_cost(works).items(): costs[work_id] += cost for work in works: costs[work.id] = work.company.currency.round(costs[work.id]) return costs @classmethod def _purchase_cost(cls, works): 'Compute direct purchase cost' pool = Pool() Currency = pool.get('currency.currency') PurchaseLine = pool.get('purchase.line') InvoiceLine = pool.get('account.invoice.line') Invoice = pool.get('account.invoice') Company = pool.get('company.company') cursor = Transaction().connection.cursor() table = cls.__table__() purchase_line = PurchaseLine.__table__() invoice_line = InvoiceLine.__table__() invoice = Invoice.__table__() company = Company.__table__() amounts = defaultdict(Decimal) work_ids = [w.id for w in works] work2currency = {} iline2work = {} for sub_ids in grouped_slice(work_ids): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(purchase_line, condition=purchase_line.work == table.id ).join(invoice_line, condition=invoice_line.origin == Concat( 'purchase.line,', purchase_line.id) ).join(invoice, condition=invoice_line.invoice == invoice.id ).select(invoice_line.id, table.id, where=where & ~invoice.state.in_(['draft', 'cancel']))) iline2work.update(cursor.fetchall()) cursor.execute(*table.join(company, condition=table.company == company.id ).select(table.id, company.currency, where=where)) work2currency.update(cursor.fetchall()) currencies = Currency.browse(set(work2currency.values())) id2currency = {c.id: c for c in currencies} invoice_lines = InvoiceLine.browse(list(iline2work.keys())) for invoice_line in invoice_lines: invoice = invoice_line.invoice work_id = iline2work[invoice_line.id] currency_id = work2currency[work_id] currency = id2currency[currency_id] if currency != invoice.currency: with Transaction().set_context(date=invoice.currency_date): amount = Currency.compute(invoice.currency, invoice_line.amount, currency) else: amount = invoice_line.amount amounts[work_id] += amount return amounts @classmethod def _get_revenue(cls, works): revenues = {} for work in works: if work.list_price: revenues[work.id] = work.company.currency.round( work.list_price * Decimal(str(work.effort_hours))) else: revenues[work.id] = Decimal(0) return revenues @fields.depends('company') def on_change_with_currency_digits(self, name=None): if self.company: return self.company.currency.digits return 2 @staticmethod def default_currency_digits(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.digits return 2 @fields.depends('product', 'company') def on_change_product(self): pool = Pool() User = pool.get('res.user') ModelData = pool.get('ir.model.data') Uom = pool.get('product.uom') Currency = pool.get('currency.currency') if not self.product: return hour_uom = Uom(ModelData.get_id('product', 'uom_hour')) self.list_price = Uom.compute_price(self.product.default_uom, self.product.list_price, hour_uom) if self.company: user = User(Transaction().user) if user.company != self.company: if user.company.currency != self.company.currency: self.list_price = Currency.compute(user.company.currency, self.list_price, self.company.currency, round=False) digits = self.__class__.list_price.digits self.list_price = self.list_price.quantize( Decimal(str(10.0 ** -digits[1])))
class Integer(ModelSQL): 'Integer' __name__ = 'test.integer' integer = fields.Integer(string='Integer', help='Test integer', required=False)
class NullOrder(ModelSQL): "Null Order" __name__ = 'test.modelsql.null_order' integer = fields.Integer('Integer')
class View(ModelSQL, ModelView): "View" __name__ = 'ir.ui.view' _rec_name = 'model' model = fields.Char('Model', select=True, states={ 'required': Eval('type').in_([None, 'tree', 'form', 'graph']), }) priority = fields.Integer('Priority', required=True, select=True) type = fields.Selection([ (None, ''), ('tree', 'Tree'), ('form', 'Form'), ('graph', 'Graph'), ('calendar', 'Calendar'), ('board', 'Board'), ], 'View Type', select=True, domain=[ If(Bool(Eval('inherit')), ('type', '=', None), ('type', '!=', None)), ], depends=['inherit']) data = fields.Text('Data') name = fields.Char('Name', states={ 'invisible': ~(Eval('module') & Eval('name')), }, depends=['module'], readonly=True) arch = fields.Function(fields.Text('View Architecture', states={ 'readonly': Bool(Eval('name')), }, depends=['name']), 'get_arch', setter='set_arch') inherit = fields.Many2One('ir.ui.view', 'Inherited View', select=True, ondelete='CASCADE') field_childs = fields.Char('Children Field', states={ 'invisible': Eval('type') != 'tree', }, depends=['type']) module = fields.Char('Module', states={ 'invisible': ~Eval('module'), }, readonly=True) domain = fields.Char('Domain', states={ 'invisible': ~Eval('inherit'), }, depends=['inherit']) @classmethod def __setup__(cls): super(View, cls).__setup__() cls._error_messages.update({ 'invalid_xml': 'Invalid XML for view "%s".', }) cls._order.insert(0, ('priority', 'ASC')) cls._buttons.update({ 'show': { 'readonly': Eval('type') != 'form', }, }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') table = TableHandler(cls, module_name) # Migration from 2.4 arch moved into data if table.column_exist('arch'): table.column_rename('arch', 'data') super(View, cls).__register__(module_name) # New instance to refresh definition table = TableHandler(cls, module_name) # Migration from 1.0 arch no more required table.not_null_action('arch', action='remove') # Migration from 2.4 model no more required table.not_null_action('model', action='remove') @staticmethod def default_priority(): return 16 @staticmethod def default_module(): return Transaction().context.get('module') or '' @classmethod @ModelView.button_action('ir.act_view_show') def show(cls, views): pass @classmethod @memoize(10) def get_rng(cls, type_): if sys.version_info < (3,): filename = __file__.decode(sys.getfilesystemencoding()) else: filename = __file__ rng_name = os.path.join(os.path.dirname(filename), type_ + '.rng') with open(rng_name, 'rb') as fp: rng = etree.fromstring(fp.read()) return rng @property def rng_type(self): if self.inherit: return self.inherit.rng_type return self.type @classmethod def validate(cls, views): super(View, cls).validate(views) cls.check_xml(views) @classmethod def check_xml(cls, views): "Check XML" for view in views: if not view.arch: continue xml = view.arch.strip() if not xml: continue try: tree = etree.fromstring(xml) except Exception: # JCA : print faulty xml try: import pprint pprint.pprint(xml) except: print(xml) raise if hasattr(etree, 'RelaxNG'): validator = etree.RelaxNG(etree=cls.get_rng(view.rng_type)) if not validator.validate(tree): error_log = '\n'.join(map(str, validator.error_log.filter_from_errors())) logger.error('Invalid XML view %s:\n%s\n%s', view.rec_name, error_log, xml) cls.raise_user_error( 'invalid_xml', (view.rec_name,), error_log) root_element = tree.getroottree().getroot() # validate pyson attributes validates = { 'states': fields.states_validate, } def encode(element): for attr in ('states', 'domain', 'spell', 'colors'): if not element.get(attr): continue try: value = PYSONDecoder().decode(element.get(attr)) validates.get(attr, lambda a: True)(value) except Exception, e: error_log = '%s: <%s %s="%s"/>' % ( e, element.get('id') or element.get('name'), attr, element.get(attr)) logger.error( 'Invalid XML view %s:\n%s\n%s', view.rec_name, error_log, xml) cls.raise_user_error( 'invalid_xml', (view.rec_name,), error_log) for child in element: encode(child) encode(root_element)
class ShipmentIn: __name__ = 'stock.shipment.in' carrier = fields.Many2One('carrier', 'Carrier', states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) cost_currency = fields.Many2One( 'currency.currency', 'Cost Currency', states={ 'required': Bool(Eval('cost')), 'readonly': ~Eval('state').in_(['draft', 'assigned', 'packed']), }, depends=['cost', 'state']) cost_currency_digits = fields.Function( fields.Integer('Cost Currency Digits'), 'on_change_with_cost_currency_digits') cost = fields.Numeric( 'Cost', digits=(16, Eval('cost_currency_digits', 2)), states={ 'readonly': ~Eval('state').in_(['draft', 'assigned', 'packed']), }, depends=['carrier', 'state', 'cost_currency_digits']) @fields.depends('currency') def on_change_with_cost_currency_digits(self, name=None): if self.cost_currency: return self.cost_currency.digits return 2 def _get_carrier_context(self): return {} @fields.depends(methods=['incoming_moves']) def on_change_carrier(self): self.on_change_incoming_moves() @fields.depends('carrier', 'incoming_moves') def on_change_incoming_moves(self): try: super(ShipmentIn, self).on_change_incoming_moves() except AttributeError: pass if not self.carrier: return with Transaction().set_context(self._get_carrier_context()): cost, currency_id = self.carrier.get_purchase_price() self.cost = cost self.cost_currency = currency_id if self.cost_currency: self.cost_currency_digits = self.cost_currency.digits else: self.cost_currency_digits = 2 def allocate_cost_by_value(self): pool = Pool() Currency = pool.get('currency.currency') Move = pool.get('stock.move') if not self.cost: return cost = Currency.compute(self.cost_currency, self.cost, self.company.currency, round=False) moves = [ m for m in self.incoming_moves if m.state not in ('done', 'cancel') ] sum_value = 0 unit_prices = {} for move in moves: unit_price = Currency.compute(move.currency, move.unit_price, self.company.currency, round=False) unit_prices[move.id] = unit_price sum_value += unit_price * Decimal(str(move.quantity)) costs = [] digit = Move.unit_price.digits[1] exp = Decimal(str(10.0**-digit)) difference = cost for move in moves: quantity = Decimal(str(move.quantity)) if not sum_value: move_cost = cost / Decimal(len(moves)) else: move_cost = cost * quantity * unit_prices[move.id] / sum_value unit_shipment_cost = (move_cost / quantity).quantize( exp, rounding=ROUND_DOWN) costs.append({ 'unit_shipment_cost': unit_shipment_cost, 'difference': move_cost - (unit_shipment_cost * quantity), 'move': move, }) difference -= unit_shipment_cost * quantity costs.sort(key=itemgetter('difference')) for cost in costs: move = cost['move'] quantity = Decimal(str(move.quantity)) if exp * quantity < difference: cost['unit_shipment_cost'] += exp difference -= exp * quantity if difference < exp: break for cost in costs: move = cost['move'] unit_shipment_cost = Currency.compute(self.company.currency, cost['unit_shipment_cost'], move.currency, round=False) unit_shipment_cost = unit_shipment_cost.quantize( exp, rounding=ROUND_HALF_EVEN) Move.write( [move], { 'unit_price': move.unit_price + unit_shipment_cost, 'unit_shipment_cost': unit_shipment_cost, }) @classmethod @ModelView.button @Workflow.transition('received') def receive(cls, shipments): Carrier = Pool().get('carrier') for shipment in shipments: if shipment.carrier: allocation_method = \ shipment.carrier.carrier_cost_allocation_method else: allocation_method = \ Carrier.default_carrier_cost_allocation_method() getattr(shipment, 'allocate_cost_by_%s' % allocation_method)() super(ShipmentIn, cls).receive(shipments)
class AvailableMedicine(ModelSQL, ModelView): "AvailableMedicine" __name__ = "hrp_inventory.available_medicine" warehouse = fields.Many2One('stock.location', 'Warehouse', domain=[('type', '=', 'warehouse')], select=True) varieties_num = fields.Integer('Varieties Num', readonly=True) lines = fields.One2Many('hrp_inventory.available_medicine_line', 'available_medicine', 'Lines') @classmethod def __setup__(cls): super(AvailableMedicine, cls).__setup__() cls._buttons.update({ 'create_the_inventory': { 'readonly': Eval('lines') != '', }, }) @classmethod @ModelView.button def create_the_inventory(self, available_medicine): Product = Pool().get('product.product') Date = Pool().get('ir.date') AvailableMedicineLine = Pool().get( 'hrp_inventory.available_medicine_line') with Transaction().set_context(stock_date_end=Date.today()): warehouse_pbl = Product.products_by_location( [available_medicine[0].warehouse.storage_location], with_childs=True) lines = 0 for key, value in warehouse_pbl.iteritems(): lines += 1 product = Product.search([('id', '=', key[1])]) if not product: continue to_create = { u'lines': lines, u'available_medicine': available_medicine[0].id, u'product': product[0].id, u'product_code': product[0].code.encode('utf-8'), u'product_name': product[0].template.name.encode('utf-8'), u'scattered_uom': product[0].default_uom.id, u'manufacturers_code': product[0].template.manufacturers_code.encode('utf-8'), u'manufacturers': product[0].template.manufacturers_describtion.encode( 'utf-8'), u'drug_specifications': product[0].template.drug_specifications, } AvailableMedicineLine.create([to_create]) AvailableMedicine.write(available_medicine, {'varieties_num': lines})
class Origin(origin_mixin(_states), ModelSQL, ModelView): "Account Statement Origin" __name__ = 'account.statement.origin' _rec_name = 'number' lines = fields.One2Many( 'account.statement.line', 'origin', "Lines", states={ 'readonly': ((Eval('statement_id', -1) < 0) | ~Eval('statement_state').in_(['draft', 'validated'])), }, domain=[ ('statement', '=', Eval('statement', -1)), ('date', '=', Eval('date', None)), ]) statement_id = fields.Function( fields.Integer("Statement ID"), 'on_change_with_statement_id') pending_amount = fields.Function(Monetary( "Pending Amount", currency='currency', digits='currency'), 'on_change_with_pending_amount', searcher='search_pending_amount') information = fields.Dict( 'account.statement.origin.information', "Information", readonly=True) @classmethod def __register__(cls, module_name): table = cls.__table_handler__(module_name) # Migration from 5.0: rename informations into information table.column_rename('informations', 'information') super(Origin, cls).__register__(module_name) @fields.depends('statement', '_parent_statement.id') def on_change_with_statement_id(self, name=None): if self.statement: return self.statement.id return -1 @fields.depends('lines', 'amount') def on_change_with_pending_amount(self, name=None): lines_amount = sum( getattr(l, 'amount') or Decimal(0) for l in self.lines) return (self.amount or Decimal(0)) - lines_amount @classmethod def search_pending_amount(cls, name, clause): pool = Pool() Line = pool.get('account.statement.line') table = cls.__table__() line = Line.__table__() _, operator, value = clause Operator = fields.SQL_OPERATORS[operator] query = (table.join(line, 'LEFT', condition=line.origin == table.id) .select(table.id, having=Operator( table.amount - Coalesce(Sum(line.amount), 0), value), group_by=table.id)) return [('id', 'in', query)] @classmethod def copy(cls, origins, default=None): default = default.copy() if default is not None else {} default.setdefault('lines') return super().copy(origins, default=default)
class AvailableMedicineLine(ModelSQL, ModelView): "AvailableMedicineLine" __name__ = "hrp_inventory.available_medicine_line" shelves_code = fields.Char('shelves_code', select=True, readonly=True, required=False) new_shelves_code = fields.Char('new_shelves_code', select=True, readonly=False, required=False) warehouse = fields.Many2One('stock.location', 'Warehouse', domain=[('type', '=', 'warehouse')], select=True) product_code = fields.Function( fields.Char('product_code', readonly=True, required=False), 'get_product_code') product_name = fields.Function( fields.Char('product_name', readonly=True, required=False), 'get_product_name') product = fields.Many2One('product.product', 'Product', domain=[('id', 'in', Eval('product_ids'))], depends=['product_ids'], select=True, states={'readonly': Bool(Eval('product'))}) product_ids = fields.Function( fields.One2Many('product.product', None, 'Products'), 'on_change_with_product_ids') safety_stock = fields.Integer('Safety stock', required=False) scattered_uom = fields.Many2One('product.uom', 'Default UOM', domain=[('category', '=', Eval('product_uom_category'))], required=False, depends=['product_uom_category']) product_uom_category = fields.Function( fields.Many2One('product.uom.category', 'Product Uom Category'), 'on_change_with_product_uom_category') drug_specifications = fields.Function( fields.Char('Drug Speic', readonly=True, required=False), 'get_drug_specifications') manufacturers_code = fields.Function( fields.Char('Manufacturers_code', readonly=True, required=False), 'get_manufacturers_code') manufacturers = fields.Function( fields.Char('Manufacturers', select=True, readonly=True, required=False), 'get_manufacturers') @staticmethod def default_warehouse(): UserId = Pool().get('hrp_internal_delivery.test_straight') return UserId.get_user_warehouse() def get_product_code(self, name): Product = Pool().get('product.product') try: product = Product.search([('id', '=', self.product.id)]) product_code = product[0].code.encode('utf-8') except: return None return product_code def get_product_name(self, name): Product = Pool().get('product.product') try: product = Product.search([('id', '=', self.product.id)]) product_name = product[0].name.encode('utf-8') except: return None return product_name def get_drug_specifications(self, name): Product = Pool().get('product.product') try: product = Product.search([('id', '=', self.product.id)]) drug_specifications = product[0].drug_specifications except: return None return drug_specifications def get_manufacturers_code(self, name): Product = Pool().get('product.product') try: product = Product.search([('id', '=', self.product.id)]) manufacturers_code = product[0].manufacturers_code.encode('utf-8') except: return None return manufacturers_code def get_manufacturers(self, name): Product = Pool().get('product.product') try: product = Product.search([('id', '=', self.product.id)]) manufacturers = product[0].manufacturers_describtion.encode( 'utf-8') except: return None return manufacturers @fields.depends('warehouse') def on_change_with_product_ids(self, name=None): Config = Pool().get('purchase.configuration') config = Config(1) OrderPoint = Pool().get('stock.order_point') if self.warehouse == config.warehouse.id: order_point = OrderPoint.search([('warehouse_location', '=', self.warehouse)]) else: order_point = OrderPoint.search([('secondary', '=', self.warehouse) ]) product_ids = [] for ids in order_point: product_ids.append(ids.product.id) return product_ids @fields.depends('product') def on_change_with_product_uom_category(self, name=None): if self.product: return self.product.default_uom_category.id @fields.depends('product', 'product_code', 'product_name', 'scattered_uom', 'drug_specifications', 'manufacturers_code', 'manufacturers') def on_change_product(self, name=None): if self.product: self.product_code = self.product.code self.product_name = self.product.name self.scattered_uom = self.product.template.default_uom self.drug_specifications = self.product.template.drug_specifications self.manufacturers_code = self.product.template.manufacturers_code self.manufacturers = self.product.template.manufacturers_describtion @classmethod def write(cls, records, values, *args): for i in records: try: values['shelves_code'] = values['new_shelves_code'] values.pop('new_shelves_code') except: pass return super(AvailableMedicineLine, cls).write(records, values) @classmethod def create(cls, vlist): Product = Pool().get('product.product') ShelvesName = Pool().get('hrp_inventory.shelves_code') for value in vlist: if value['product'] and value['warehouse']: availble = AvailableMedicineLine.search([ ('product', '=', value['product']), ('warehouse', '=', value['warehouse']) ]) if value['product']: product = Product.search([('id', '=', value['product'])]) value['scattered_uom'] = product[0].template.default_uom.id if value['new_shelves_code']: shelves_code = ShelvesName.search([ ('name', '=', value['new_shelves_code']) ]) if not shelves_code: ShelvesName.create([{'name': value['new_shelves_code']}]) if not availble: try: value['shelves_code'] = value['new_shelves_code'] value.pop('new_shelves_code') except: pass else: cls.raise_user_error(u'该表中药品必须唯一,标识为:%s' % availble[0].id) return super(AvailableMedicineLine, cls).create(vlist)
class ComputerLoan(Workflow, ModelSQL, ModelView): 'Computer Loan' __name__ = 'computer.loan' salary_code = fields.Char('Salary Code', states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state']) employee = fields.Many2One('company.employee', 'Name of Applicant', states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state']) designation = fields.Many2One("employee.designation", "Applicant's Designation", states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state']) department = fields.Many2One('company.department', 'Department') pay_in_band = fields.Char('Pay in the Pay Band') price = fields.Float('Price of Personal Computer') amount_required = fields.Float("Amount Required", states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state'], required=True) date_of_retirement = fields.Date('Date of Retirement') dob = fields.Date("Date of Birth", states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state']) installment_no = fields.Integer("Number of installment") purpose = fields.Selection([ ('', 'None'), ('yes', 'Yes'), ('no', 'No'), ], string='Purpose', states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state'], required=True) drawal_date = fields.Date('Date of drawal of advance', states={ 'invisible': ~Eval('purpose').in_(['yes']), 'required': Eval('purpose').in_(['yes']), }, depends=['purpose']) interest = fields.Float('Interest', states={ 'invisible': ~Eval('purpose').in_(['yes']), 'required': Eval('purpose').in_(['yes']), }, depends=['purpose']) basic_pay = fields.Float("Basic Pay", states={ 'readonly': ~Eval('state').in_(['draft']), }, depends=['state']) cancel_reason = fields.Many2One( 'loan.cancel.reason', 'Cancel Reason', states={ 'invisible': ~Eval('state').in_(['forwarded_to_ao', 'cancel']), 'readonly': ~Eval('state').in_(['forwarded_to_ao']), }, depends=['state'], ) payout = fields.Float('Payout', states={ 'readonly': ~Eval('state').in_(['draft']), 'invisible': ~Eval('refund').in_(['refundable']), }, depends=['state']) pending = fields.Float('Pending', states={ 'readonly': ~Eval('state').in_(['draft']), 'invisible': ~Eval('refund').in_(['refundable']), }, depends=['state']) reschedule = fields.Float('Reschedule', states={ 'readonly': ~Eval('state').in_(['draft']), 'invisible': ~Eval('refund').in_(['refundable']), }, depends=['state']) state = fields.Selection([('draft', 'Draft'), ('forwarded_to_jo', 'Forwarded to JO'), ('forwarded_to_ao', 'Forwarded to AO'), ('approve', 'Approved'), ('cancel', 'Cancel')], 'Status', readonly=True) @classmethod def __setup__(cls): super().__setup__() cls._transitions |= set(( ('draft', 'forwarded_to_jo'), ('forwarded_to_jo', 'forwarded_to_ao'), ('forwarded_to_ao', 'approve'), ('forwarded_to_ao', 'cancel'), )) cls._buttons.update({ 'submitted_to_ao': { 'invisible': ~Eval('state').in_(['draft']), 'depends': ['state'], }, 'forward_to_jo': { 'invisible': ~Eval('state').in_(['forwarded_to_jo']), 'depends': ['state'], }, 'forward_to_ao': { 'invisible': ~Eval('state').in_(['forwarded_to_ao']), 'depends': ['state'], }, 'cancel': { 'invisible': ~Eval('state').in_(['forwarded_to_ao']), 'depends': ['state'], }, }) @staticmethod def default_state(): return 'draft' @classmethod @ModelView.button @Workflow.transition('forwarded_to_jo') def submitted_to_ao(cls, records): cls.loan_installment(records) pass @classmethod @ModelView.button @Workflow.transition('forwarded_to_ao') def forward_to_jo(cls, records): pass @classmethod @ModelView.button @Workflow.transition('approve') def forward_to_ao(cls, records): pass @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, records): for record in records: if not record.cancel_reason: cls.raise_user_error('Please fill the Cancel reason') pass @staticmethod def default_employee(): pool = Pool() User = pool.get('res.user') user = User(Transaction().user) employee = user.employee return employee.id if employee else None @fields.depends('employee') def on_change_employee(self, name=None): self.salary_code = self.employee.salary_code self.designation = self.employee.designation self.department = self.employee.department self.pay_in_band = self.employee.pay_in_band pool = Pool() hrcontract = pool.get('hr.contract') contracts = hrcontract.search([('employee', '=', self.employee), ('active', '<=', True)]) for contract in contracts: self.basic_pay = contract.basic @classmethod def write(cls, *args): actions = iter(args) for mechanisms, values in zip(actions, actions): if 'installment_no' in values.keys(): cls.change_loan_installment(mechanisms, values) super(ComputerLoan, cls).write(*args) @classmethod def change_loan_installment(cls, records, values): cursor = Transaction().connection.cursor() LoanLine = Pool().get('loan.line') for loan in records: cursor.execute( 'SELECT sum(amount) FROM loan_line WHERE loan=%s \ AND status = %s', (loan.id, 'done')) total_amount = cursor.fetchone() if total_amount[0]: reschedule = loan.amount_required - total_amount[0] cls.write(records, { 'payout': total_amount[0], 'reschedule': reschedule }) amount = (reschedule / values['installment_no']) else: amount = (loan.amount_required / values['installment_no']) cursor.execute( 'delete FROM loan_line WHERE loan=%s \ AND status != %s', (loan.id, 'done')) count = 0 for line in range(1, int(values['installment_no']) + 1): mydate = datetime.datetime.now().month month = mydate - 1 if month + line > 12: count += 1 if count > 12: count = 1 months = datetime.date(1900, count, 1).strftime('%B') else: months = datetime.date(1900, month + line, 1).strftime('%B') vals = { 'month': months, 'amount': amount, 'status': 'pending', 'loan': loan.id } line = LoanLine.create([vals]) @classmethod def loan_installment(cls, records): count = 0 LoanLine = Pool().get('loan.line') for loan in records: amount = (loan.amount_required / loan.installment_no) for line in range(1, int(loan.installment_no) + 1): mydate = datetime.datetime.now().month month = mydate - 1 if month + line > 12: count += 1 if count > 12: count = 1 months = datetime.date(1900, count, 1).strftime('%B') else: months = datetime.date(1900, month + line, 1).strftime('%B') vals = { 'month': months, 'amount': amount, 'status': 'pending', 'loan': loan.id } line = LoanLine.create([vals])
class Account(DeactivableMixin, tree('distribution_parents'), tree(), ModelSQL, ModelView): 'Analytic Account' __name__ = 'analytic_account.account' name = fields.Char('Name', required=True, translate=True, select=True) code = fields.Char('Code', select=True) company = fields.Many2One('company.company', 'Company', required=True) currency = fields.Function( fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') type = fields.Selection([ ('root', 'Root'), ('view', 'View'), ('normal', 'Normal'), ('distribution', 'Distribution'), ], 'Type', required=True) root = fields.Many2One('analytic_account.account', 'Root', select=True, domain=[ ('company', '=', Eval('company', -1)), ('parent', '=', None), ('type', '=', 'root'), ], states={ 'invisible': Eval('type') == 'root', 'required': Eval('type') != 'root', }, depends=['company', 'type']) parent = fields.Many2One('analytic_account.account', 'Parent', select=True, domain=[ 'OR', ('root', '=', Eval('root', -1)), ('parent', '=', None), ], states={ 'invisible': Eval('type') == 'root', 'required': Eval('type') != 'root', }, depends=['root', 'type']) childs = fields.One2Many('analytic_account.account', 'parent', 'Children', states={ 'invisible': Eval('id', -1) < 0, }, domain=[ ('company', '=', Eval('company', -1)), ], depends=['company']) balance = fields.Function( fields.Numeric('Balance', digits=(16, Eval('currency_digits', 1)), depends=['currency_digits']), 'get_balance') credit = fields.Function( fields.Numeric('Credit', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_credit_debit') debit = fields.Function( fields.Numeric('Debit', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_credit_debit') state = fields.Selection([ ('draft', 'Draft'), ('opened', 'Opened'), ('closed', 'Closed'), ], 'State', required=True) note = fields.Text('Note') distributions = fields.One2Many('analytic_account.account.distribution', 'parent', "Distributions", states={ 'invisible': Eval('type') != 'distribution', 'required': Eval('type') == 'distribution', }, depends=['type']) distribution_parents = fields.Many2Many( 'analytic_account.account.distribution', 'account', 'parent', "Distribution Parents", readonly=True) @classmethod def __setup__(cls): super(Account, cls).__setup__() cls._order.insert(0, ('code', 'ASC')) cls._order.insert(1, ('name', 'ASC')) @classmethod def __register__(cls, module_name): super(Account, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 4.0: remove currency table.not_null_action('currency', action='remove') # Migration from 5.0: remove display_balance table.drop_column('display_balance') @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_type(): return 'normal' @staticmethod def default_state(): return 'draft' @classmethod def validate(cls, accounts): super(Account, cls).validate(accounts) for account in accounts: account.check_distribution() def check_distribution(self): if self.type != 'distribution': return if sum((d.ratio for d in self.distributions)) != 1: raise AccountValidationError( gettext('analytic_account.msg_invalid_distribution', account=self.rec_name)) @fields.depends('company') def on_change_with_currency(self, name=None): if self.company: return self.company.currency.id @fields.depends('company') def on_change_with_currency_digits(self, name=None): if self.company: return self.company.currency.digits return 2 @fields.depends('parent', 'type', '_parent_parent.root', '_parent_parent.type') def on_change_parent(self): if self.parent and self.type != 'root': if self.parent.type == 'root': self.root = self.parent else: self.root = self.parent.root else: self.root = None @classmethod def get_balance(cls, accounts, name): pool = Pool() Line = pool.get('analytic_account.line') MoveLine = pool.get('account.move.line') cursor = Transaction().connection.cursor() table = cls.__table__() line = Line.__table__() move_line = MoveLine.__table__() ids = [a.id for a in accounts] childs = cls.search([('parent', 'child_of', ids)]) all_ids = list({}.fromkeys(ids + [c.id for c in childs]).keys()) id2account = {} all_accounts = cls.browse(all_ids) for account in all_accounts: id2account[account.id] = account line_query = Line.query_get(line) cursor.execute( *table.join(line, 'LEFT', condition=table.id == line.account).join( move_line, 'LEFT', condition=move_line.id == line.move_line). select(table.id, Sum(Coalesce(line.credit, 0) - Coalesce(line.debit, 0)), where=(table.type != 'view') & table.id.in_(all_ids) & (table.active == True) & line_query, group_by=table.id)) account_sum = defaultdict(Decimal) for account_id, value in cursor.fetchall(): account_sum.setdefault(account_id, Decimal('0.0')) # SQLite uses float for SUM if not isinstance(value, Decimal): value = Decimal(str(value)) account_sum[account_id] += value balances = {} for account in accounts: balance = Decimal() childs = cls.search([ ('parent', 'child_of', [account.id]), ]) for child in childs: balance += account_sum[child.id] exp = Decimal(str(10.0**-account.currency_digits)) balances[account.id] = balance.quantize(exp) return balances @classmethod def get_credit_debit(cls, accounts, names): pool = Pool() Line = pool.get('analytic_account.line') MoveLine = pool.get('account.move.line') cursor = Transaction().connection.cursor() table = cls.__table__() line = Line.__table__() move_line = MoveLine.__table__() result = {} ids = [a.id for a in accounts] for name in names: if name not in ('credit', 'debit'): raise Exception('Bad argument') result[name] = {}.fromkeys(ids, Decimal('0.0')) id2account = {} for account in accounts: id2account[account.id] = account line_query = Line.query_get(line) columns = [table.id] for name in names: columns.append(Sum(Coalesce(Column(line, name), 0))) cursor.execute( *table.join(line, 'LEFT', condition=table.id == line.account).join( move_line, 'LEFT', condition=move_line.id == line.move_line).select(*columns, where=(table.type != 'view') & table.id.in_(ids) & (table.active == True) & line_query, group_by=table.id)) for row in cursor.fetchall(): account_id = row[0] for i, name in enumerate(names, 1): value = row[i] # SQLite uses float for SUM if not isinstance(value, Decimal): value = Decimal(str(value)) result[name][account_id] += value for account in accounts: for name in names: exp = Decimal(str(10.0**-account.currency_digits)) result[name][account.id] = ( result[name][account.id].quantize(exp)) return result def get_rec_name(self, name): if self.code: return self.code + ' - ' + str(self.name) else: return str(self.name) @classmethod def search_rec_name(cls, name, clause): accounts = cls.search([('code', ) + tuple(clause[1:])], limit=1) if accounts: return [('code', ) + tuple(clause[1:])] else: return [(cls._rec_name, ) + tuple(clause[1:])] def distribute(self, amount): "Return a list of (account, amount) distribution" assert self.type in {'normal', 'distribution'} if self.type == 'normal': return [(self, amount)] else: result = [] remainder = amount for distribution in self.distributions: account = distribution.account ratio = distribution.ratio current_amount = self.currency.round(amount * ratio) remainder -= current_amount result.extend(account.distribute(current_amount)) if remainder: i = 0 while remainder: account, amount = result[i] rounding = self.currency.rounding.copy_sign(remainder) result[i] = (account, amount - rounding) remainder -= rounding i = (i + 1) % len(result) return result
class Complaint(Workflow, ModelSQL, ModelView): 'Customer Complaint' __name__ = 'sale.complaint' _rec_name = 'number' _states = { 'readonly': Eval('state') != 'draft', } number = fields.Char('Number', readonly=True, select=True) reference = fields.Char('Reference', select=True) date = fields.Date('Date', states=_states) customer = fields.Many2One('party.party', "Customer", required=True, states=_states, context={ 'company': Eval('company', -1), }, depends={'company'}) address = fields.Many2One('party.address', 'Address', domain=[('party', '=', Eval('customer'))], states=_states) company = fields.Many2One('company.company', 'Company', required=True, states=_states) employee = fields.Many2One('company.employee', 'Employee', states=_states) type = fields.Many2One('sale.complaint.type', 'Type', required=True, states=_states) origin = fields.Reference( 'Origin', selection='get_origin', domain={ 'sale.sale': [ If(Eval('customer'), ('party', '=', Eval('customer')), ()), ('company', '=', Eval('company')), ('state', 'in', ['confirmed', 'processing', 'done']), ], 'sale.line': [ ('type', '=', 'line'), If(Eval('customer'), ('sale.party', '=', Eval('customer')), ()), ('sale.company', '=', Eval('company')), ('sale.state', 'in', ['confirmed', 'processing', 'done']), ], 'account.invoice': [ If(Eval('customer'), ('party', '=', Eval('customer')), ()), ('company', '=', Eval('company')), ('type', '=', 'out'), ('state', 'in', ['posted', 'paid']), ], 'account.invoice.line': [ ('type', '=', 'line'), If(Eval('customer'), ('invoice.party', '=', Eval('customer')), ()), ('invoice.company', '=', Eval('company')), ('invoice.type', '=', 'out'), ('invoice.state', 'in', ['posted', 'paid']), ], }, states={ 'readonly': ((Eval('state') != 'draft') | Bool(Eval('actions', [0]))), 'required': Bool(Eval('origin_model')), }, depends={'origin_model'}) origin_id = fields.Function(fields.Integer('Origin ID'), 'on_change_with_origin_id') origin_model = fields.Function(fields.Char('Origin Model'), 'on_change_with_origin_model') description = fields.Text('Description', states=_states) actions = fields.One2Many( 'sale.complaint.action', 'complaint', 'Actions', states={ 'readonly': ((Eval('state') != 'draft') | (If(~Eval('origin_id', 0), 0, Eval('origin_id', 0)) <= 0)), }, depends={'origin_model'}) state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('done', 'Done'), ('cancelled', 'Cancelled'), ], "State", readonly=True, required=True, sort=False) @classmethod def __setup__(cls): super(Complaint, cls).__setup__() cls._order.insert(0, ('date', 'DESC')) cls._transitions |= set(( ('draft', 'waiting'), ('waiting', 'draft'), ('waiting', 'approved'), ('waiting', 'rejected'), ('approved', 'done'), ('approved', 'draft'), ('draft', 'cancelled'), ('waiting', 'cancelled'), ('done', 'draft'), ('rejected', 'draft'), ('cancelled', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'waiting']), 'depends': ['state'], }, 'draft': { 'invisible': ~Eval('state').in_(['waiting', 'done', 'cancelled']), 'icon': If( Eval('state').in_(['done', 'cancelled']), 'tryton-undo', 'tryton-back'), 'depends': ['state'], }, 'wait': { 'invisible': ~Eval('state').in_(['draft']), 'depends': ['state'], }, 'approve': { 'invisible': ~Eval('state').in_(['waiting']), 'depends': ['state'], }, 'reject': { 'invisible': ~Eval('state').in_(['waiting']), 'depends': ['state'], }, 'process': { 'invisible': ~Eval('state').in_(['approved']), 'depends': ['state'], }, }) actions_domains = cls._actions_domains() actions_domain = [('action', 'in', actions_domains.pop(None))] for model, actions in actions_domains.items(): actions_domain = If( Eval('origin_model') == model, [('action', 'in', actions)], actions_domain) cls.actions.domain = [actions_domain] @classmethod def __register__(cls, module_name): table_h = cls.__table_handler__(module_name) # Migration from 3.8: rename reference into number if (table_h.column_exist('reference') and not table_h.column_exist('number')): table_h.column_rename('reference', 'number') super(Complaint, cls).__register__(module_name) @classmethod def _actions_domains(cls): return { None: [], 'sale.sale': ['sale_return'], 'sale.line': ['sale_return'], 'account.invoice': ['credit_note'], 'account.invoice.line': ['credit_note'], } @staticmethod def default_date(): pool = Pool() Date = pool.get('ir.date') return Date.today() @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_state(): return 'draft' @fields.depends('type') def get_origin(self): if self.type: origin = self.type.origin return [('', ''), (origin.model, origin.name)] else: return [] @fields.depends('origin', 'customer') def on_change_origin(self): pool = Pool() Sale = pool.get('sale.sale') SaleLine = pool.get('sale.line') Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') if not self.customer and self.origin and self.origin.id >= 0: if isinstance(self.origin, Sale): self.customer = self.origin.party elif isinstance(self.origin, SaleLine): self.customer = self.origin.sale.party elif isinstance(self.origin, Invoice): self.customer = self.origin.party elif isinstance(self.origin, InvoiceLine) and self.origin.invoice: self.customer = self.origin.invoice.party @fields.depends('origin') def on_change_with_origin_id(self, name=None): if self.origin: return self.origin.id @fields.depends('origin') def on_change_with_origin_model(self, name=None): if self.origin: return self.origin.__class__.__name__ @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')), ] @classmethod def create(cls, vlist): pool = Pool() Configuration = pool.get('sale.configuration') config = Configuration(1) vlist = [v.copy() for v in vlist] default_company = cls.default_company() for values in vlist: if values.get('number') is None: values['number'] = config.get_multivalue( 'complaint_sequence', company=values.get('company', default_company)).get() return super(Complaint, cls).create(vlist) @classmethod def copy(cls, complaints, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) return super(Complaint, cls).copy(complaints, default=default) @classmethod def delete(cls, complaints): for complaint in complaints: if complaint.state != 'draft': raise AccessError( gettext('sale_complaint.msg_complaint_delete_draft', complaint=complaint.rec_name)) super(Complaint, cls).delete(complaints) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('waiting') def wait(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('approved') def approve(cls, complaints): pool = Pool() Configuration = pool.get('sale.configuration') transaction = Transaction() context = transaction.context config = Configuration(1) with transaction.set_context( queue_scheduled_at=config.sale_process_after, queue_batch=context.get('queue_batch', True)): cls.__queue__.process(complaints) @classmethod @ModelView.button @Workflow.transition('rejected') def reject(cls, complaints): pass @classmethod @ModelView.button @Workflow.transition('done') def process(cls, complaints): pool = Pool() Action = pool.get('sale.complaint.action') results = defaultdict(list) actions = defaultdict(list) for complaint in complaints: for action in complaint.actions: if action.result: continue result = action.do() results[result.__class__].append(result) actions[result.__class__].append(action) for kls, records in results.items(): kls.save(records) for action, record in zip(actions[kls], records): action.result = record Action.save(sum(list(actions.values()), []))
class AnalyticMixin(object): analytic_accounts = fields.One2Many('analytic.account.entry', 'origin', 'Analytic Accounts', size=Eval('analytic_accounts_size', 0), depends=['analytic_accounts_size']) analytic_accounts_size = fields.Function( fields.Integer('Analytic Accounts Size'), 'get_analytic_accounts_size') @classmethod def __register__(cls, module_name): pool = Pool() AccountEntry = pool.get('analytic.account.entry') cursor = Transaction().connection.cursor() super(AnalyticMixin, cls).__register__(module_name) handler = cls.__table_handler__(module_name) # Migration from 3.4: analytic accounting changed to reference field if handler.column_exist('analytic_accounts'): entry = AccountEntry.__table__() table = cls.__table__() cursor.execute( *table.select(table.id, table.analytic_accounts, where=table.analytic_accounts != None)) for line_id, selection_id in cursor.fetchall(): cursor.execute( *entry.update(columns=[entry.origin], values=['%s,%s' % (cls.__name__, line_id)], where=entry.selection == selection_id)) handler.drop_column('analytic_accounts') @classmethod def analytic_accounts_domain(cls): context = Transaction().context.copy() context['context'] = context return PYSONDecoder(context).decode(PYSONEncoder().encode( cls.analytic_accounts.domain)) @classmethod def default_analytic_accounts(cls): pool = Pool() AnalyticAccount = pool.get('analytic_account.account') accounts = [] root_accounts = AnalyticAccount.search(cls.analytic_accounts_domain() + [ ('parent', '=', None), ]) for account in root_accounts: accounts.append({ 'root': account.id, }) return accounts @classmethod def default_analytic_accounts_size(cls): pool = Pool() AnalyticAccount = pool.get('analytic_account.account') return len( AnalyticAccount.search(cls.analytic_accounts_domain() + [ ('type', '=', 'root'), ])) @classmethod def get_analytic_accounts_size(cls, records, name): roots = cls.default_analytic_accounts_size() return {r.id: roots for r in records}
class ModelInfo(ModelView): 'Model Name' __name__ = 'ir.model.debug.model_info' model_name = fields.Selection('get_possible_model_names', 'Model Name') field_infos = fields.One2Many('ir.model.debug.model_info.field_info', '', 'Fields Infos') hide_functions = fields.Boolean('Hide Functions') filter_value = fields.Selection([('name', 'Name'), ('kind', 'Kind'), ('string', 'String')], 'Filter Value') id_to_calculate = fields.Integer('Id To Calculate') to_evaluate = fields.Char( 'To Evaluate', states={'invisible': ~Bool(Eval('id_to_calculate', False))}, help="Use the 'instance' keyword to get the instanciated model", depends=['id_to_calculate']) evaluation_result = fields.Text( 'Evaluation Result', states={'invisible': ~Bool(Eval('id_to_calculate', False))}, readonly=True, depends=['id_to_calculate']) must_raise_exception = fields.Boolean('Must Raise Exception') previous_runs = fields.Text( 'Previous runs', states={'invisible': ~Bool(Eval('id_to_calculate', False))}, readonly=True, depends=['id_to_calculate']) @classmethod def __setup__(cls): super(ModelInfo, cls).__setup__() cls.__rpc__.update({ 'raw_model_infos': RPC(), 'raw_module_infos': RPC(), 'raw_field_infos': RPC(), }) @classmethod def get_possible_model_names(cls): pool = Pool() return list([ (x, x) for x in pool._pool[pool.database_name]['model'].iterkeys() ]) def get_field_info(self, field, field_name): info = Pool().get('ir.model.debug.model_info.field_info')() info.name = field_name info.string = field.string if isinstance(field, fields.Function): if self.hide_functions: return None info.is_function = True real_field = field._field else: info.is_function = False real_field = field info.kind = real_field.__class__.__name__ if isinstance(field, (fields.Many2One, fields.One2Many)): info.target_model = field.model_name elif isinstance(field, fields.Many2Many): if field.target: info.target_model = Pool().get( field.relation_name)._fields[field.target].model_name else: info.target_model = field.relation_name else: info.target_model = '' for elem in ('required', 'readonly', 'invisible'): setattr(info, 'is_%s' % elem, getattr(field, elem, False)) setattr(info, 'state_%s' % elem, repr(field.states.get(elem, {}))) field_domain = getattr(field, 'domain', None) if field_domain: info.has_domain = True info.field_domain = repr(field_domain) return info @classmethod def default_filter_value(cls): return 'name' @fields.depends('model_name', 'hide_functions', 'filter_value', 'field_infos', 'id_to_calculate') def on_change_filter_value(self): self.recalculate_field_infos() @fields.depends('model_name', 'hide_functions', 'filter_value', 'field_infos', 'id_to_calculate') def on_change_hide_functions(self): self.recalculate_field_infos() @fields.depends('model_name', 'hide_functions', 'filter_value', 'field_infos', 'id_to_calculate') def on_change_model_name(self): self.to_evaluate = '' self.evaluation_result = '' self.recalculate_field_infos() @fields.depends('model_name', 'id_to_calculate', 'to_evaluate', 'must_raise_exception', 'previous_runs') def on_change_to_evaluate(self): if not self.to_evaluate: self.evaluation_result = '' return if not self.id_to_calculate or not self.model_name: self.evaluation_result = '' self.previous_runs = '' return if self.previous_runs: previous_runs = self.previous_runs[1:].split('\n\n' + '#' * 20 + '\n\n') else: previous_runs = [''] if previous_runs[0] != self.to_evaluate: self.previous_runs = '\n' + ('\n\n' + '#' * 20 + '\n\n').join( [self.to_evaluate] + [x for x in previous_runs if x]) try: context = { 'instance': Pool().get(self.model_name)(self.id_to_calculate), } self.evaluation_result = pprint.pformat( eval(self.to_evaluate, context)) except Exception, exc: if self.must_raise_exception: raise self.evaluation_result = 'ERROR: %s' % str(exc)
class NereidStaticFile(ModelSQL, ModelView): "Static files for Nereid" __name__ = "nereid.static.file" name = fields.Char('File Name', select=True, required=True) folder = fields.Many2One('nereid.static.folder', 'Folder', select=True, required=True) #: This function field returns the field contents. This is useful if the #: field is going to be displayed on the clients. file_binary = fields.Function( fields.Binary('File', filename='name'), 'get_file_binary', 'set_file_binary', ) #: Full path to the file in the filesystem file_path = fields.Function(fields.Char('File Path'), 'get_file_path') #: URL that can be used to idenfity the resource. Note that the value #: of this field is available only when called within a request context. #: In other words the URL is valid only when called in a nereid request. url = fields.Function(fields.Char('URL'), 'get_url') # Sequence sequence = fields.Integer('Sequence', select=True) # File mimetype mimetype = fields.Function(fields.Char('Mimetype'), getter='get_mimetype') @classmethod def __setup__(cls): super(NereidStaticFile, cls).__setup__() cls._sql_constraints += [ ('name_folder_uniq', 'UNIQUE(name, folder)', 'The Name of the Static File must be unique in a folder.!'), ] cls._error_messages.update({ 'invalid_file_name': """Invalid file name: (1) '..' in file name (OR) (2) file name contains '/'""", }) @staticmethod def default_sequence(): return 10 def get_mimetype(self, name): """ This method detects and returns the mimetype for the static file. The python mimetypes module returns a tuple of the form -: >>> mimetypes.guess_type(file_name) (file_mimetype, encoding) which can then be used to fill the `mimetype` field. Some example types are -: * image/png * application/pdf etc. """ return mimetypes.guess_type(self.name)[0] def get_url(self, name): """Return the url if within an active request context or return False values """ if _request_ctx_stack.top is None: return None return url_for('nereid.static.file.send_static_file', folder=self.folder.name, name=self.name) @staticmethod def get_nereid_base_path(): """ Returns base path for nereid, where all the static files would be stored. By Default it is: <Tryton Data Path>/<Database Name>/nereid """ cursor = Transaction().cursor return os.path.join(config.get('database', 'path'), cursor.database_name, "nereid") def _set_file_binary(self, value): """ Setter for static file that stores file in file system :param value: The value to set """ file_binary = fields.Binary.cast(bytes(value)) # If the folder does not exist, create it recursively directory = os.path.dirname(self.file_path) if not os.path.isdir(directory): os.makedirs(directory) with open(self.file_path, 'wb') as file_writer: file_writer.write(file_binary) @classmethod def set_file_binary(cls, files, name, value): """ Setter for the functional binary field. :param files: Records :param name: Ignored :param value: The file buffer """ for static_file in files: static_file._set_file_binary(value) def get_file_binary(self, name): ''' Getter for the binary_file field. This fetches the file from the file system, coverts it to buffer and returns it. :param name: Field name :return: Bytes ''' location = self.file_path with open(location, 'rb') as file_reader: return fields.Binary.cast(file_reader.read()) def get_file_path(self, name): """ Returns the full path to the file in the file system :param name: Field name :return: File path """ return os.path.abspath( os.path.join(self.get_nereid_base_path(), self.folder.name, self.name)) @classmethod def validate(cls, files): """ Validates the records. :param files: active record list of static files """ super(NereidStaticFile, cls).validate(files) for file in files: file.check_file_name() def check_file_name(self): ''' Check the validity of folder name Allowing the use of / or . will be risky as that could eventually lead to previlege escalation ''' if ('..' in self.name) or ('/' in self.name): self.raise_user_error("invalid_file_name") @classmethod @route("/static-file/<folder>/<name>", methods=["GET"]) def send_static_file(cls, folder, name): """ Invokes the send_file method in nereid.helpers to send a file as the response to the request. The file is sent in a way which is as efficient as possible. For example nereid will use the X-Send_file header to make nginx send the file if possible. :param folder: name of the folder :param name: name of the file """ # TODO: Separate this search and find into separate cached method files = cls.search([('folder.name', '=', folder), ('name', '=', name)]) if not files: abort(404) return send_file(files[0].file_path)
class Sample(metaclass=PoolMeta): __name__ = 'lims.sample' plant = fields.Function(fields.Many2One('lims.plant', 'Plant'), 'get_plant', searcher='search_plant') equipment = fields.Many2One('lims.equipment', 'Equipment', domain=['OR', ('id', '=', Eval('equipment')), ('party', '=', Eval('party'))], depends=['party'], select=True) equipment_template = fields.Function(fields.Many2One( 'lims.equipment.template', 'Equipment Template'), 'get_equipment_field') equipment_model = fields.Function(fields.Char('Equipment Model'), 'get_equipment_field') equipment_serial_number = fields.Function(fields.Char( 'Equipment Serial Number'), 'get_equipment_field') equipment_name = fields.Function(fields.Char( 'Equipment Name'), 'get_equipment_field') component = fields.Many2One('lims.component', 'Component', domain=['OR', ('id', '=', Eval('component')), ('equipment', '=', Eval('equipment'))], depends=['equipment']) comercial_product = fields.Many2One('lims.comercial.product', 'Comercial Product') ind_sampling_date = fields.Date('Sampling date') ind_volume = fields.Float('Received volume') sampling_type = fields.Many2One('lims.sampling.type', 'Sampling Type') ind_operational_detail = fields.Text('Operational detail') ind_work_environment = fields.Text('Work environment') ind_analysis_reason = fields.Text('Reason for analysis') missing_data = fields.Boolean('Missing data') attributes_domain = fields.Function(fields.Many2Many( 'lims.sample.attribute', None, None, 'Attributes domain'), 'on_change_with_attributes_domain') sample_photo = fields.Binary('Sample Photo', file_id='sample_photo_id', store_prefix='sample') sample_photo_id = fields.Char('Sample Photo ID', readonly=True) label_photo = fields.Binary('Label Photo', file_id='label_photo_id', store_prefix='sample') label_photo_id = fields.Char('Label Photo ID', readonly=True) oil_added = fields.Float('Liters Oil added') ind_equipment = fields.Integer('Equipment') ind_equipment_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) ind_component = fields.Integer('Component') ind_component_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) ind_oil = fields.Integer('Oil') ind_oil_uom = fields.Selection([ ('hs', 'Hs.'), ('km', 'Km.'), ], 'UoM', sort=False) oil_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Oil?', sort=False) oil_changed_string = oil_changed.translated('oil_changed') oil_filter_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Oil Filter?', sort=False) oil_filter_changed_string = oil_filter_changed.translated( 'oil_filter_changed') air_filter_changed = fields.Selection([ (None, '-'), ('yes', 'Yes'), ('no', 'No'), ], 'Did change Air Filter?', sort=False) air_filter_changed_string = air_filter_changed.translated( 'air_filter_changed') edition_log = fields.One2Many('lims.sample.edition.log', 'sample', 'Edition log', readonly=True) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table_h = cls.__table_handler__(module_name) sample = cls.__table__() super().__register__(module_name) if table_h.column_exist('changed_oil'): cursor.execute(*sample.update([sample.oil_changed], [Case((sample.changed_oil == Literal(True), 'yes'), else_='no')])) table_h.drop_column('changed_oil') if table_h.column_exist('changed_oil_filter'): cursor.execute(*sample.update([sample.oil_filter_changed], [Case((sample.changed_oil_filter == Literal(True), 'yes'), else_='no')])) table_h.drop_column('changed_oil_filter') if table_h.column_exist('changed_air_filter'): cursor.execute(*sample.update([sample.air_filter_changed], [Case((sample.changed_air_filter == Literal(True), 'yes'), else_='no')])) table_h.drop_column('changed_air_filter') if table_h.column_exist('hours_equipment'): cursor.execute(*sample.update([sample.ind_equipment], [sample.hours_equipment])) table_h.drop_column('hours_equipment') if table_h.column_exist('hours_component'): cursor.execute(*sample.update([sample.ind_component], [sample.hours_component])) table_h.drop_column('hours_component') if table_h.column_exist('hours_oil'): cursor.execute(*sample.update([sample.ind_oil], [sample.hours_oil])) table_h.drop_column('hours_oil') @classmethod def __setup__(cls): super().__setup__() cls.product_type.states['readonly'] = Bool(Eval('component')) if 'component' not in cls.product_type.depends: cls.product_type.depends.append('component') cls.matrix.states['readonly'] = Bool(Eval('comercial_product')) if 'comercial_product' not in cls.matrix.depends: cls.matrix.depends.append('comercial_product') cls.attributes.domain = [('id', 'in', Eval('attributes_domain'))] if 'attributes_domain' not in cls.attributes.depends: cls.attributes.depends.append('attributes_domain') @staticmethod def default_ind_equipment_uom(): return 'hs' @staticmethod def default_ind_component_uom(): return 'hs' @staticmethod def default_ind_oil_uom(): return 'hs' @fields.depends('equipment', 'component') def on_change_equipment(self): if not self.equipment and self.component: self.component = None @fields.depends('component') def on_change_component(self): if self.component: if self.component.product_type: self.product_type = self.component.product_type.id if self.component.comercial_product: self.comercial_product = self.component.comercial_product.id self.on_change_comercial_product() @fields.depends('comercial_product') def on_change_comercial_product(self): if self.comercial_product and self.comercial_product.matrix: self.matrix = self.comercial_product.matrix.id @fields.depends('product_type', '_parent_product_type.attribute_set') def on_change_with_attributes_domain(self, name=None): pool = Pool() SampleAttributeAttributeSet = pool.get( 'lims.sample.attribute-attribute.set') attribute_set = None if self.product_type and self.product_type.attribute_set: attribute_set = self.product_type.attribute_set.id res = SampleAttributeAttributeSet.search([ ('attribute_set', '=', attribute_set), ]) return [x.attribute.id for x in res] @classmethod def get_plant(cls, samples, name): result = {} for s in samples: result[s.id] = s.equipment and s.equipment.plant.id or None return result @classmethod def search_plant(cls, name, clause): return [('equipment.plant',) + tuple(clause[1:])] def _order_equipment_field(name): def order_field(tables): Equipment = Pool().get('lims.equipment') field = Equipment._fields[name] table, _ = tables[None] equipment_tables = tables.get('equipment') if equipment_tables is None: equipment = Equipment.__table__() equipment_tables = { None: (equipment, equipment.id == table.equipment), } tables['equipment'] = equipment_tables return field.convert_order(name, equipment_tables, Equipment) return staticmethod(order_field) order_plant = _order_equipment_field('plant') @classmethod def get_equipment_field(cls, samples, names): result = {} for name in names: result[name] = {} if cls._fields[name]._type == 'many2one': for s in samples: field = getattr(s.equipment, name.replace( 'equipment_', ''), None) result[name][s.id] = field.id if field else None else: for s in samples: result[name][s.id] = getattr(s.equipment, name.replace( 'equipment_', ''), None) return result @classmethod def order_component(cls, tables): Component = Pool().get('lims.component') kind_field = Component._fields['kind'] location_field = Component._fields['location'] sample, _ = tables[None] component_tables = tables.get('component') if component_tables is None: component = Component.__table__() component_tables = { None: (component, component.id == sample.component), } tables['component'] = component_tables order = ( kind_field.convert_order('kind', component_tables, Component) + location_field.convert_order('location', component_tables, Component)) return order @classmethod def _confirm_samples(cls, samples): TaskTemplate = Pool().get('lims.administrative.task.template') for sample in samples: if not sample.component or not sample.comercial_product: continue if sample.component.comercial_product != sample.comercial_product: sample.component.comercial_product = sample.comercial_product sample.component.save() TaskTemplate.create_tasks('sample_missing_data', cls._for_task_missing_data(samples)) TaskTemplate.create_tasks('sample_insufficient_volume', cls._for_task_required_volume(samples)) @classmethod def _for_task_missing_data(cls, samples): AdministrativeTask = Pool().get('lims.administrative.task') res = [] for sample in samples: if not sample.missing_data: continue if AdministrativeTask.search([ ('type', '=', 'sample_missing_data'), ('origin', '=', '%s,%s' % (cls.__name__, sample.id)), ('state', 'not in', ('done', 'discarded')), ]): continue res.append(sample) return res @classmethod def _for_task_required_volume(cls, samples): pool = Pool() EntryDetailAnalysis = pool.get('lims.entry.detail.analysis') AdministrativeTask = pool.get('lims.administrative.task') res = [] for sample in samples: received_volume = sample.ind_volume or 0 analysis_detail = EntryDetailAnalysis.search([ ('sample', '=', sample.id)]) for detail in analysis_detail: received_volume -= (detail.analysis.ind_volume or 0) if received_volume >= 0: continue if AdministrativeTask.search([ ('type', '=', 'sample_insufficient_volume'), ('origin', '=', '%s,%s' % (cls.__name__, sample.id)), ('state', 'not in', ('done', 'discarded')), ]): continue res.append(sample) return res @classmethod def delete(cls, samples): AdministrativeTask = Pool().get('lims.administrative.task') for sample in samples: tasks = AdministrativeTask.search([ ('origin', '=', '%s,%s' % (cls.__name__, sample.id)), ('state', 'not in', ('done', 'discarded')), ]) if tasks: AdministrativeTask.write(tasks, {'state': 'draft'}) AdministrativeTask.delete(tasks) super().delete(samples)
class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView): 'Payment Term Line' __name__ = 'account.invoice.payment_term.line' payment = fields.Many2One('account.invoice.payment_term', 'Payment Term', required=True, ondelete="CASCADE") type = fields.Selection([ ('fixed', 'Fixed'), ('percent', 'Percentage on Remainder'), ('percent_on_total', 'Percentage on Total'), ('remainder', 'Remainder'), ], 'Type', required=True) ratio = fields.Numeric( 'Ratio', digits=(14, 10), states={ 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), 'required': Eval('type').in_(['percent', 'percent_on_total']), }, depends=['type']) divisor = fields.Numeric( 'Divisor', digits=(10, 14), states={ 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), 'required': Eval('type').in_(['percent', 'percent_on_total']), }, depends=['type']) amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('type') != 'fixed', 'required': Eval('type') == 'fixed', }, depends=['type', 'currency_digits']) currency = fields.Many2One('currency.currency', 'Currency', states={ 'invisible': Eval('type') != 'fixed', 'required': Eval('type') == 'fixed', }, depends=['type']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') relativedeltas = fields.One2Many('account.invoice.payment_term.line.delta', 'line', 'Deltas') @classmethod def __register__(cls, module_name): sql_table = cls.__table__() super(PaymentTermLine, cls).__register__(module_name) cursor = Transaction().connection.cursor() table = cls.__table_handler__(module_name) # Migration from 3.8: rename percentage into ratio if table.column_exist('percentage'): cursor.execute(*sql_table.update( columns=[sql_table.ratio], values=[sql_table.percentage / 100])) table.drop_column('percentage') @staticmethod def default_currency_digits(): return 2 @staticmethod def default_type(): return 'remainder' @classmethod def default_relativedeltas(cls): if Transaction().user == 0: return [] return [{}] @fields.depends('type') def on_change_type(self): if self.type != 'fixed': self.amount = Decimal('0.0') self.currency = None if self.type not in ('percent', 'percent_on_total'): self.ratio = Decimal('0.0') self.divisor = Decimal('0.0') @fields.depends('ratio') def on_change_ratio(self): if not self.ratio: self.divisor = Decimal('0.0') else: self.divisor = self.round(1 / self.ratio, self.__class__.divisor.digits[1]) @fields.depends('divisor') def on_change_divisor(self): if not self.divisor: self.ratio = Decimal('0.0') else: self.ratio = self.round(1 / self.divisor, self.__class__.ratio.digits[1]) @fields.depends('currency') def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 def get_date(self, date): for relativedelta_ in self.relativedeltas: date += relativedelta_.get() return date def get_value(self, remainder, amount, currency): Currency = Pool().get('currency.currency') if self.type == 'fixed': fixed = Currency.compute(self.currency, self.amount, currency) return fixed.copy_sign(amount) elif self.type == 'percent': return currency.round(remainder * self.ratio) elif self.type == 'percent_on_total': return currency.round(amount * self.ratio) elif self.type == 'remainder': return currency.round(remainder) return None @staticmethod def round(number, digits): quantize = Decimal(10)**-Decimal(digits) return Decimal(number).quantize(quantize) @classmethod def validate(cls, lines): super(PaymentTermLine, cls).validate(lines) cls.check_ratio_and_divisor(lines) @classmethod def check_ratio_and_divisor(cls, lines): "Check consistency between ratio and divisor" # Use a copy because on_change will change the records for line in cls.browse(lines): if line.type not in ('percent', 'percent_on_total'): continue if line.ratio is None or line.divisor is None: raise PaymentTermValidationError( gettext( 'account_invoice' '.msg_payment_term_invalid_ratio_divisor', line=line.rec_name)) if (line.ratio != round(1 / line.divisor, cls.ratio.digits[1]) and line.divisor != round(1 / line.ratio, cls.divisor.digits[1])): raise PaymentTermValidationError( gettext( 'account_invoice' '.msg_payment_term_invalid_ratio_divisor', line=line.rec_name))