class UIMenu( DeactivableMixin, sequence_ordered(order='ASC NULLS LAST'), tree(separator=' / '), ModelSQL, ModelView): "UI menu" __name__ = 'ir.ui.menu' name = fields.Char('Menu', required=True, translate=True) childs = fields.One2Many('ir.ui.menu', 'parent', 'Children') parent = fields.Many2One('ir.ui.menu', 'Parent Menu', select=True, ondelete='CASCADE') groups = fields.Many2Many('ir.ui.menu-res.group', 'menu', 'group', 'Groups') complete_name = fields.Function(fields.Char('Complete Name'), 'get_rec_name', searcher='search_rec_name') icon = fields.Selection('list_icons', 'Icon', translate=False) action = fields.Function(fields.Reference('Action', selection=[ ('', ''), ('ir.action.report', 'ir.action.report'), ('ir.action.act_window', 'ir.action.act_window'), ('ir.action.wizard', 'ir.action.wizard'), ('ir.action.url', 'ir.action.url'), ], translate=False), 'get_action', setter='set_action') action_keywords = fields.One2Many('ir.action.keyword', 'model', 'Action Keywords') favorite = fields.Function(fields.Boolean('Favorite'), 'get_favorite') @classmethod def order_complete_name(cls, tables): return cls.name.convert_order('name', tables, cls) @staticmethod def default_icon(): return 'tryton-folder' @classmethod def default_sequence(cls): return 50 @staticmethod def list_icons(): pool = Pool() Icon = pool.get('ir.ui.icon') return sorted(CLIENT_ICONS + [(name, name) for _, name in Icon.list_icons()]) @classmethod def search_global(cls, text): # TODO improve search clause for record in cls.search([ ('rec_name', 'ilike', '%%%s%%' % text), ]): if record.action: yield record, record.rec_name, record.icon @classmethod def search(cls, domain, offset=0, limit=None, order=None, count=False, query=False): menus = super(UIMenu, cls).search(domain, offset=offset, limit=limit, order=order, count=False, query=query) if query: return menus if menus: parent_ids = {x.parent.id for x in menus if x.parent} parents = set() for sub_parent_ids in grouped_slice(parent_ids): parents.update(cls.search([ ('id', 'in', list(sub_parent_ids)), ])) # Re-browse to avoid side-cache access menus = cls.browse([x.id for x in menus if (x.parent and x.parent in parents) or not x.parent]) if count: return len(menus) return menus @classmethod def get_action(cls, menus, name): pool = Pool() actions = dict((m.id, None) for m in menus) with Transaction().set_context(active_test=False): menus = cls.browse(menus) action_keywords = sum((list(m.action_keywords) for m in menus), []) def action_type(keyword): return keyword.action.type action_keywords.sort(key=action_type) for type, action_keywords in groupby(action_keywords, key=action_type): action_keywords = list(action_keywords) for action_keyword in action_keywords: model = action_keyword.model actions[model.id] = '%s,-1' % type Action = pool.get(type) action2keyword = {k.action.id: k for k in action_keywords} with Transaction().set_context(active_test=False): factions = Action.search([ ('action', 'in', list(action2keyword.keys())), ]) for action in factions: model = action2keyword[action.id].model actions[model.id] = str(action) return actions @classmethod def set_action(cls, menus, name, value): pool = Pool() ActionKeyword = pool.get('ir.action.keyword') action_keywords = [] transaction = Transaction() for i in range(0, len(menus), transaction.database.IN_MAX): sub_menus = menus[i:i + transaction.database.IN_MAX] action_keywords += ActionKeyword.search([ ('keyword', '=', 'tree_open'), ('model', 'in', [str(menu) for menu in sub_menus]), ]) if action_keywords: with Transaction().set_context(_timestamp=False): ActionKeyword.delete(action_keywords) if not value: return if isinstance(value, str): action_type, action_id = value.split(',') else: action_type, action_id = value if int(action_id) <= 0: return Action = pool.get(action_type) action = Action(int(action_id)) to_create = [] for menu in menus: with Transaction().set_context(_timestamp=False): to_create.append({ 'keyword': 'tree_open', 'model': str(menu), 'action': action.action.id, }) if to_create: ActionKeyword.create(to_create) @classmethod def get_favorite(cls, menus, name): pool = Pool() Favorite = pool.get('ir.ui.menu.favorite') user = Transaction().user favorites = Favorite.search([ ('menu', 'in', [m.id for m in menus]), ('user', '=', user), ]) menu2favorite = dict((m.id, False if m.action else None) for m in menus) menu2favorite.update(dict((f.menu.id, True) for f in favorites)) return menu2favorite
class Subscription(Workflow, ModelSQL, ModelView): "Subscription" __name__ = 'sale.subscription' _rec_name = 'number' company = fields.Many2One( 'company.company', "Company", required=True, select=True, states={ 'readonly': Eval('state') != 'draft', }, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=['state'], help="Make the subscription belong to the company.") number = fields.Char( "Number", readonly=True, select=True, help="The main identification of the subscription.") # TODO revision reference = fields.Char( "Reference", select=True, help="The identification of an external origin.") description = fields.Char("Description", states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) party = fields.Many2One( 'party.party', "Party", required=True, states={ 'readonly': ((Eval('state') != 'draft') | (Eval('lines', [0]) & Eval('party'))), }, depends=['state'], help="The party who subscribes.") invoice_address = fields.Many2One( 'party.address', "Invoice Address", domain=[ ('party', '=', Eval('party')), ], states={ 'readonly': Eval('state') != 'draft', 'required': ~Eval('state').in_(['draft']), }, depends=['party', 'state']) payment_term = fields.Many2One( 'account.invoice.payment_term', "Payment Term", states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) currency = fields.Many2One( 'currency.currency', "Currency", required=True, states={ 'readonly': ((Eval('state') != 'draft') | (Eval('lines', [0]) & Eval('currency', 0))), }, depends=['state']) start_date = fields.Date( "Start Date", required=True, states={ 'readonly': ((Eval('state') != 'draft') | Eval('next_invoice_date')), }, depends=['state', 'next_invoice_date']) end_date = fields.Date( "End Date", domain=['OR', ('end_date', '>=', If( Bool(Eval('start_date')), Eval('start_date', datetime.date.min), datetime.date.min)), ('end_date', '=', None), ], states={ 'readonly': Eval('state') != 'draft', }, depends=['start_date', 'state']) invoice_recurrence = fields.Many2One( 'sale.subscription.recurrence.rule.set', "Invoice Recurrence", required=True, states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) invoice_start_date = fields.Date("Invoice Start Date", states={ 'readonly': ((Eval('state') != 'draft') | Eval('next_invoice_date')), }, depends=['state', 'next_invoice_date']) next_invoice_date = fields.Date("Next Invoice Date", readonly=True) lines = fields.One2Many( 'sale.subscription.line', 'subscription', "Lines", states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) state = fields.Selection( STATES, "State", readonly=True, required=True, help="The current state of the subscription.") @classmethod def __setup__(cls): super(Subscription, cls).__setup__() cls._order = [ ('start_date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= set(( ('draft', 'canceled'), ('draft', 'quotation'), ('quotation', 'canceled'), ('quotation', 'draft'), ('quotation', 'running'), ('running', 'draft'), ('running', 'closed'), ('canceled', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['draft', 'quotation']), 'icon': 'tryton-cancel', }, 'draft': { 'invisible': Eval('state').in_(['draft', 'closed']), 'icon': If(Eval('state') == 'canceled', 'tryton-clear', 'tryton-go-previous'), }, 'quote': { 'invisible': Eval('state') != 'draft', 'readonly': ~Eval('lines', []), 'icon': 'tryton-go-next', }, 'run': { 'invisible': Eval('state') != 'quotation', 'icon': 'tryton-go-next', }, }) @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_currency(cls): pool = Pool() Company = pool.get('company.company') company = cls.default_company() if company: return Company(company).currency.id @classmethod def default_state(cls): return 'draft' @fields.depends('party') def on_change_party(self): self.invoice_address = None if self.party: self.invoice_address = self.party.address_get(type='invoice') self.payment_term = self.party.customer_payment_term @classmethod def set_number(cls, subscriptions): pool = Pool() Sequence = pool.get('ir.sequence') Config = pool.get('sale.configuration') config = Config(1) for subscription in subscriptions: if subscription.number: continue subscription.number = Sequence.get_id( config.subscription_sequence.id) cls.save(subscriptions) def compute_next_invoice_date(self): start_date = self.invoice_start_date or self.start_date date = self.next_invoice_date or self.start_date rruleset = self.invoice_recurrence.rruleset(start_date) dt = datetime.datetime.combine(date, datetime.time()) inc = (start_date == date) and not self.next_invoice_date next_date = rruleset.after(dt, inc=inc) return next_date.date() @classmethod def copy(cls, subscriptions, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('state', 'draft') default.setdefault('number') default.setdefault('next_invoice_date') return super(Subscription, cls).copy(subscriptions, default=default) @classmethod @ModelView.button @Workflow.transition('canceled') def cancel(cls, subscriptions): pass @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, subscriptions): pass @classmethod @ModelView.button @Workflow.transition('quotation') def quote(cls, subscriptions): cls.set_number(subscriptions) @classmethod @ModelView.button @Workflow.transition('running') def run(cls, subscriptions): pool = Pool() Line = pool.get('sale.subscription.line') lines = [] for subscription in subscriptions: if not subscription.next_invoice_date: subscription.next_invoice_date = ( subscription.compute_next_invoice_date()) for line in subscription.lines: if (line.next_consumption_date is None and not line.consumed): line.next_consumption_date = ( line.compute_next_consumption_date()) lines.extend(subscription.lines) Line.save(lines) cls.save(subscriptions) @classmethod def process(cls, subscriptions): to_close = [] for subscription in subscriptions: if all(l.next_consumption_date is None for l in subscription.lines): to_close.append(subscription) cls.close(to_close) @classmethod @Workflow.transition('closed') def close(cls, subscriptions): pass @classmethod def generate_invoice(cls, date=None): pool = Pool() Date = pool.get('ir.date') Consumption = pool.get('sale.subscription.line.consumption') Invoice = pool.get('account.invoice') InvoiceLine = pool.get('account.invoice.line') if date is None: date = Date.today() consumptions = Consumption.search([ ('invoice_line', '=', None), ('line.subscription.next_invoice_date', '<=', date), ('line.subscription.state', 'in', ['running', 'closed']), ], order=[ ('line.subscription.id', 'DESC'), ]) def keyfunc(consumption): return consumption.line.subscription invoices = {} lines = {} for subscription, consumptions in groupby(consumptions, key=keyfunc): invoices[subscription] = invoice = subscription._get_invoice() lines[subscription] = Consumption.get_invoice_lines( consumptions, invoice) all_invoices = invoices.values() Invoice.save(all_invoices) all_invoice_lines = [] for subscription, invoice in invoices.iteritems(): invoice_lines, _ = lines[subscription] for line in invoice_lines: line.invoice = invoice all_invoice_lines.extend(invoice_lines) InvoiceLine.save(all_invoice_lines) all_consumptions = [] for values in lines.itervalues(): for invoice_line, consumptions in zip(*values): for consumption in consumptions: assert not consumption.invoice_line consumption.invoice_line = invoice_line all_consumptions.append(consumption) Consumption.save(all_consumptions) Invoice.update_taxes(all_invoices) subscriptions = cls.search([ ('next_invoice_date', '<=', date), ]) for subscription in subscriptions: if subscription.state == 'running': while subscription.next_invoice_date <= date: subscription.next_invoice_date = ( subscription.compute_next_invoice_date()) else: subscription.next_invoice_date = None cls.save(subscriptions) def _get_invoice(self): pool = Pool() Invoice = pool.get('account.invoice') invoice = Invoice( company=self.company, type='out', party=self.party, invoice_address=self.invoice_address, currency=self.currency, account=self.party.account_receivable, ) invoice.on_change_type() invoice.payment_term = self.payment_term return invoice
class Group(metaclass=PoolMeta): __name__ = 'account.payment.group' sepa_messages = fields.One2Many('account.payment.sepa.message', 'origin', 'SEPA Messages', readonly=True, domain=[('company', '=', Eval('company', -1))], states={ 'invisible': ~Eval('sepa_messages'), }, depends=['company']) @classmethod def __setup__(cls): super(Group, cls).__setup__() cls._buttons.update({ 'generate_message': {}, }) def get_sepa_template(self): if self.kind == 'payable': return loader.load('%s.xml' % self.journal.sepa_payable_flavor) elif self.kind == 'receivable': return loader.load('%s.xml' % self.journal.sepa_receivable_flavor) def process_sepa(self): pool = Pool() Payment = pool.get('account.payment') if self.kind == 'receivable': mandates = Payment.get_sepa_mandates(self.payments) for payment, mandate in zip(self.payments, mandates): if not mandate: raise ProcessError( gettext( 'account_payment_sepa' '.msg_payment_process_no_mandate', payment=payment.rec_name)) # Write one by one because mandate.sequence_type must be # recomputed each time Payment.write( [payment], { 'sepa_mandate': mandate, 'sepa_mandate_sequence_type': mandate.sequence_type, }) else: for payment in self.payments: if not payment.sepa_bank_account_number: raise ProcessError( gettext( 'account_payment_sepa' '.msg_payment_process_no_iban', payment=payment.rec_name)) self.generate_message(_save=False) @dualmethod @ModelView.button def generate_message(cls, groups, _save=True): pool = Pool() Message = pool.get('account.payment.sepa.message') for group in groups: tmpl = group.get_sepa_template() if not tmpl: raise NotImplementedError if not group.sepa_messages: group.sepa_messages = () message = tmpl.generate( group=group, datetime=datetime, normalize=unicodedata.normalize, ).filter(remove_comment).render() message = Message(message=message, type='out', state='waiting', company=group.company) group.sepa_messages += (message, ) if _save: cls.save(groups) @property def sepa_initiating_party(self): return self.company.party def sepa_group_payment_key(self, payment): key = (('date', payment.date), ) if self.kind == 'receivable': key += (('sequence_type', payment.sepa_mandate_sequence_type), ) key += (('scheme', payment.sepa_mandate.scheme), ) return key def sepa_group_payment_id(self, key): payment_id = str(key['date'].toordinal()) if self.kind == 'receivable': payment_id += '-' + key['sequence_type'] return payment_id @property def sepa_payments(self): pool = Pool() Payment = pool.get('account.payment') keyfunc = self.sepa_group_payment_key # re-browse to align cache payments = Payment.browse(sorted(self.payments, key=keyfunc)) for key, grouped_payments in groupby(payments, key=keyfunc): yield dict(key), list(grouped_payments)
class PaymentTermLine(ModelSQL, ModelView): 'Payment Term Line' __name__ = 'account.invoice.payment_term.line' sequence = fields.Integer('Sequence', help='Use to order lines in ascending order') 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) percentage = fields.Numeric( 'Percentage', digits=(16, 8), states={ 'invisible': ~Eval('type').in_(['percent', 'percent_on_total']), 'required': Eval('type').in_(['percent', 'percent_on_total']), }, depends=['type']) divisor = fields.Numeric( 'Divisor', digits=(16, 8), 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.relativedelta', 'line', 'Deltas') @classmethod def __setup__(cls): super(PaymentTermLine, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) cls._error_messages.update({ 'invalid_percentage_and_divisor': ('Percentage and ' 'Divisor values are not consistent in line "%(line)s" ' 'of payment term "%(term)s".'), }) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') sql_table = cls.__table__() super(PaymentTermLine, cls).__register__(module_name) cursor = Transaction().cursor table = TableHandler(cursor, cls, module_name) # Migration from 1.0 percent change into percentage if table.column_exist('percent'): cursor.execute(*sql_table.update(columns=[sql_table.percentage], values=[sql_table.percent * 100])) table.drop_column('percent', exception=True) # Migration from 2.2 if table.column_exist('delay'): cursor.execute( *sql_table.update(columns=[sql_table.day], values=[31], where=sql_table.delay == 'end_month')) table.drop_column('delay', exception=True) lines = cls.search([]) for line in lines: if line.percentage: cls.write( [line], { 'divisor': cls.round( Decimal('100.0') / line.percentage, cls.divisor.digits[1]), }) # 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 == Null, table.sequence] @staticmethod def default_currency_digits(): return 2 @staticmethod def default_type(): return 'remainder' @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.percentage = Decimal('0.0') self.divisor = Decimal('0.0') @fields.depends('percentage') def on_change_percentage(self): if not self.percentage: self.divisor = 0.0 else: self.divisor = self.round( Decimal('100.0') / self.percentage, self.__class__.divisor.digits[1]) @fields.depends('divisor') def on_change_divisor(self): if not self.divisor: self.percentage = 0.0 else: self.percentage = self.round( Decimal('100.0') / self.divisor, self.__class__.percentage.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_delta(self): return { 'day': self.day, 'month': int(self.month) if self.month else None, 'days': self.days, 'weeks': self.weeks, 'months': self.months, 'weekday': int(self.weekday) if self.weekday else None, } 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': return Currency.compute(self.currency, self.amount, currency) elif self.type == 'percent': return currency.round(remainder * self.percentage / Decimal('100')) elif self.type == 'percent_on_total': return currency.round(amount * self.percentage / Decimal('100')) 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_percentage_and_divisor(lines) @classmethod def check_percentage_and_divisor(cls, lines): "Check consistency between percentage and divisor" percentage_digits = cls.percentage.digits[1] divisor_digits = cls.divisor.digits[1] for line in lines: if line.type not in ('percent', 'percent_on_total'): continue if line.percentage is None or line.divisor is None: cls.raise_user_error('invalid_percentage_and_divisor', { 'line': line.rec_name, 'term': line.payment.rec_name, }) if line.percentage == line.divisor == Decimal('0.0'): continue percentage = line.percentage divisor = line.divisor calc_percentage = cls.round( Decimal('100.0') / divisor, percentage_digits) calc_divisor = cls.round( Decimal('100.0') / percentage, divisor_digits) if (percentage == Decimal('0.0') or divisor == Decimal('0.0') or percentage != calc_percentage and divisor != calc_divisor): cls.raise_user_error('invalid_percentage_and_divisor', { 'line': line.rec_name, 'term': line.payment.rec_name, })
class LotAttributeType(ModelSQL, ModelView): 'Lot Attribute Type' __name__ = 'stock.lot.attribute.type' name = fields.Char('Name', required=True) attributes = fields.One2Many('stock.lot.attribute', 'type', 'Attributes')
class Receipt(Workflow, ModelSQL, ModelView): "Cash/Bank Receipt" __name__ = "cash_bank.receipt" _states = { 'readonly': Eval('state') != 'draft', } _depends = ['state'] 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) cash_bank = fields.Many2One( 'cash_bank.cash_bank', 'Cash/Bank', required=True, domain=[ ('company', 'in', [Eval('context', {}).get('company', -1), None]) ], states={ 'readonly': Bool(Eval('lines')) | Bool(Eval('state') != 'draft') }, depends=_depends + ['lines']) type = fields.Many2One('cash_bank.receipt_type', 'Type', required=True, domain=[ If(Bool(Eval('cash_bank')), [('cash_bank', '=', Eval('cash_bank'))], [('id', '=', -1)] ), ], states={ 'readonly': Bool(Eval('lines')) | Bool(Eval('state') != 'draft') }, depends=_depends + ['cash_bank', 'lines']) type_type = fields.Function(fields.Char('Type of Cash/Bank type', size=None), 'on_change_with_type_type') currency = fields.Many2One('currency.currency', 'Currency', required=True, states={'readonly': True}) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') number = fields.Char('Number', size=None, readonly=True, select=True) reference = fields.Char('Reference', size=None) description = fields.Char('Description', size=None, states=_states, depends=_depends) date = fields.Date('Date', required=True, states=_states, depends=_depends) party = fields.Many2One('party.party', 'Party', ondelete='RESTRICT', states={ 'readonly': Eval('state') != 'draft', 'required': Bool(Eval('party_required')) }, depends=_depends + ['party_required']) party_required = fields.Function(fields.Boolean('Party Required'), 'on_change_with_party_required') bank_account = fields.Many2One('bank.account', 'Bank Account', states={ 'readonly': Eval('state') != 'draft', 'invisible': Not(Bool(Eval('bank_account_show'))), 'required': Bool(Eval('bank_account_required')) }, domain=[ ('id', 'in', Eval('bank_account_owners')) ], depends=_depends + ['party', 'bank_account_show', 'bank_account_owners', 'bank_account_required']) bank_account_show = fields.Function(fields.Boolean('Bank Account Show'), 'on_change_with_bank_account_show') bank_account_owners = fields.Function(fields.One2Many('bank.account', None, 'Bank Account Owners'), 'on_change_with_bank_account_owners', setter='set_bank_account_owners') bank_account_required = fields.Function(fields.Boolean( 'Bank Account Required'), 'on_change_with_bank_account_required') cash = fields.Numeric('Cash', digits=(16, Eval('_parent_receipt', {}).get('currency_digits', 2)), states=_states, depends=_depends + ['currency_digits']) documents = fields.Many2Many('cash_bank.document-cash_bank.receipt', 'receipt', 'document', 'Documents', domain=[ If(Eval('state') != 'posted', [ [('convertion', '=', None)], If(Bool(Eval('type')), If(Eval('type_type') == 'in', ['OR', [('last_receipt', '=', None)], [('last_receipt.id', '=', Eval('id'))], [('last_receipt.type.type', '=', 'out')] ], ['OR', [('last_receipt.id', '=', Eval('id'))], [ ('last_receipt.type.type', '=', 'in'), ('last_receipt.state', 'in', ['confirmed', 'posted']) ] ], ), [('id', '=', -1)] ), ], [('id', '!=', -1)] ) ], states=_states, depends=_depends + ['id', 'type', 'type_type']) total_documents = fields.Function(fields.Numeric('Total Documents', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total_detail') document_allow = fields.Function(fields.Boolean('Allow documents'), 'on_change_with_document_allow') lines = fields.One2Many('cash_bank.receipt.line', 'receipt', 'Lines', states={ 'readonly': (Not(Bool(Eval('cash_bank'))) | Not(Bool(Eval('type'))) | Bool(Eval('state') != 'draft')) }, depends=_depends + ['cash_bank', 'type']) total_lines = fields.Function(fields.Numeric('Total Lines', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total_detail') total = fields.Function(fields.Numeric('Total', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_total') diff = fields.Function(fields.Numeric('Diff', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_diff') move = fields.Many2One('account.move', 'Move', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ], depends=['company']) line_move = fields.Many2One('account.move.line', 'Account Move Line', readonly=True) state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('posted', 'Posted'), ('cancel', 'Canceled'), ], 'State', readonly=True, required=True) transfer = fields.Many2One('cash_bank.transfer', 'Transfer', readonly=True) attachments = fields.One2Many('ir.attachment', 'resource', 'Attachments') logs = fields.One2Many('cash_bank.receipt.log_action', 'resource', 'Logs', readonly=True) del _states, _depends @classmethod def __register__(cls, module_name): super(Receipt, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 5.2.1: if table.column_exist('made_by'): cls._migrate_log() table.drop_column('made_by') table.drop_column('confirmed_by') table.drop_column('posted_by') table.drop_column('canceled_by') @classmethod def _migrate_log(cls): def add_log(Log, User, receipt, user, action, logs, create=False): if not user: return user = User(user) if create: date = receipt.create_date else: date = receipt.write_date log = Log( resource=receipt, date=date, user=user, action=action ) logs.append(log) pool = Pool() Log = pool.get('cash_bank.receipt.log_action') User = pool.get('res.user') logs = [] cursor = Transaction().connection.cursor() sql = "SELECT id, made_by, confirmed_by, canceled_by, posted_by " \ "FROM cash_bank_receipt" cursor.execute(sql) records = cursor.fetchall() for row in records: rcp = cls(row[0]) add_log(Log, User, rcp, row[1], 'Created', logs, True) add_log(Log, User, rcp, row[2], 'Confirmed', logs) add_log(Log, User, rcp, row[4], 'Posted', logs) add_log(Log, User, rcp, row[3], 'Cancelled', logs) Log.save(logs) @classmethod def __setup__(cls): super(Receipt, cls).__setup__() cls._order = [ ('date', 'DESC'), ('number', 'DESC'), ] cls._transitions |= set( ( ('draft', 'confirmed'), ('confirmed', 'posted'), ('confirmed', 'cancel'), ('cancel', 'draft'), ) ) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['confirmed']), 'readonly': Bool(Eval('transfer')), }, 'confirm': { 'invisible': ~Eval('state').in_(['draft']), }, 'post': { 'invisible': ~Eval('state').in_(['confirmed']), 'readonly': Bool(Eval('transfer')), }, '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_lines(): return Decimal('0.0') @staticmethod def default_total_documents(): return Decimal('0.0') @staticmethod def default_total(): return Decimal('0.0') @staticmethod def default_diff(): 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('type') def on_change_with_type_type(self, name=None): if self.type: return self.type.type @fields.depends('type', 'documents', 'state') def on_change_with_document_allow(self, name=None): if self.type: if self.type.type == 'in': return True else: if self.documents and self.state != 'draft': return True @fields.depends('type') def on_change_with_party_required(self, name=None): if self.type: return self.type.party_required @fields.depends('type', 'bank_account_show') def on_change_with_bank_account_required(self, name=None): if self.type and self.bank_account_show: if self.bank_account_show == True: return self.type.bank_account_required @fields.depends('type', 'party_required') def on_change_with_bank_account_show(self, name=None): if self.type and self.party_required: if self.party_required == True: return self.type.bank_account @fields.depends('type', 'party') def on_change_with_bank_account_owners(self, name=None): if self.type and self.party: if self.party.bank_accounts: res = [] for acc in self.party.bank_accounts: res.append(acc.id) return res return [] @classmethod def set_bank_account_owners(cls, lines, name, value): pass @fields.depends() def on_change_company(self): self.cash_bank = None self.type = None @fields.depends() def on_change_cash_bank(self): self.type = None @fields.depends('type') def on_change_type(self): if self.type: self.cash_bank = self.type.cash_bank @fields.depends('cash', 'total_documents', 'total_lines') def on_change_cash(self): if not self.cash: self.cash = Decimal('0.0') self._set_total( self.total_documents, self.total_lines) @fields.depends('documents', 'cash', 'total_lines') def on_change_documents(self): self.total_documents = \ self.get_total_detail('total_documents') self._set_total( self.total_documents, self.total_lines) @fields.depends('lines', 'cash', 'total_documents') def on_change_lines(self): self.total_lines = \ self.get_total_detail('total_lines') self._set_total( self.total_documents, self.total_lines) def get_total_detail(self, name): name = name[6:] # Remove 'total_' from begining total = self._get_total_details(getattr(self, name)) return total def get_total(self, name=None): total = self.cash + self.total_documents return total def get_diff(self, name=None): return self.total_lines - self.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 view_attributes(cls): return super(Receipt, cls).view_attributes() + [ ('//page[@name="documents"]', 'states', { 'invisible': ~Eval('document_allow'), })] def _get_total_details(self, details): result = Decimal('0.0') if details: for detail in details: if detail.amount: result += detail.amount return result def _set_total(self, total_documents, total_lines): self.total = Decimal('0.0') diff = Decimal('0.0') if self.cash: self.total += self.cash if total_lines: diff += total_lines if total_documents: self.total += total_documents self.diff = diff - self.total def _get_move(self): 'Return Move for Receipt' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') period_id = Period.find(self.company.id, date=self.date) move = Move( period=period_id, journal=self.cash_bank.journal_cash_bank, date=self.date, origin=self, company=self.company, description=self.description, ) return move, period_id def _get_move_line(self, period): pool = Pool() MoveLine = pool.get('account.move.line') Currency = pool.get('currency.currency') debit = Decimal('0.0') credit = Decimal('0.0') with Transaction().set_context(date=self.date): amount = Currency.compute(self.currency, self.total, self.company.currency) if self.currency != self.company.currency: second_currency = self.currency amount_second_currency = self.total else: amount_second_currency = None second_currency = None if self.type.type == 'in': debit = amount else: credit = amount description = self.description if self.reference: if description: description += ' / ' description += self.reference return MoveLine( period=period, debit=debit, credit=credit, account=self.cash_bank.account, second_currency=second_currency, amount_second_currency=amount_second_currency, description=description, ) @classmethod def create(cls, vlist): receipts = super(Receipt, cls).create(vlist) write_log('log_action.msg_created', receipts) return receipts @classmethod def validate(cls, receipts): super(Receipt, cls).validate(receipts) cls.set_document_receipt(receipts) @classmethod def set_document_receipt(cls, receipts): def doc_exists(id_, docs): for dc in docs: if dc.id == id_: return True Document = Pool().get('cash_bank.document') lasts = {} for receipt in receipts: for doc in receipt.documents: if doc.last_receipt != receipt: doc.last_receipt = receipt doc.save() if receipt.transfer and \ receipt.transfer.state in ['confirmed', 'post']: pass else: if receipt.rec_name not in lasts: lasts[receipt.rec_name] = [] lasts[receipt.rec_name].append(doc) # Verify if any document have been deleted from list # so last_receipt must be updated documents = Document.search([ ('last_receipt', '=', receipt.id)]) for doc in documents: if not doc_exists(doc.id, receipt.documents): doc.set_previous_receipt() doc.save() for key, value in lasts.items(): write_log('Asigned to Receipt: ' + key, value) @classmethod def set_number(cls, receipts): pool = Pool() Sequence = pool.get('ir.sequence') for receipt in receipts: if receipt.number: continue receipt.number = \ Sequence.get_id(receipt.type.sequence.id) cls.save(receipts) @classmethod def delete(cls, receipts): pool = Pool() Attachment = pool.get('ir.attachment') atts = [] for receipt in receipts: if receipt.state not in ['draft']: write_log('log_action.msg_deletion_attempt', [receipt]) raise UserError( gettext('cash_bank.msg_delete_document_cash_bank', doc_name='Receipt', doc_number=receipt.rec_name, state='Draft' )) for doc in receipt.documents: doc.set_previous_receipt() doc.save() for att in receipt.attachments: atts.append(att) Attachment.delete(atts) super(Receipt, cls).delete(receipts) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, receipts): write_log('log_action.msg_draft', receipts) @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, receipts): for receipt in receipts: if not receipt.lines: raise UserError( gettext('cash_bank.msg_receipt_no_lines', receipt=receipt.rec_name )) if receipt.diff != 0: raise UserError( gettext('cash_bank.msg_diff_total_lines_cash_bank' )) if receipt.total < 0: raise UserError( gettext('cash_bank.msg_total_less_zero' )) if receipt.cash < 0: raise UserError( gettext('cash_bank.msg_cash_less_zero' )) for doc in receipt.documents: if doc.amount <= 0: raise UserError( gettext('cash_bank.msg_document_less_equal_zero' )) if receipt.type.party_required and not receipt.party: raise UserError( gettext('cash_bank.msg_party_required_cash_bank' )) move, period = receipt._get_move() move.save() receipt_line_move = receipt._get_move_line(period) receipt_line_move.move = move receipt_line_move.save() move_lines = [receipt_line_move] for line in receipt.lines: line.validate_line() move_line = line.get_move_line(period) move_line.move = move move_line.save() line.line_move = move_line line.save() move_lines.append(move_line) move.lines = move_lines move.save() receipt.move = move receipt.line_move = receipt_line_move receipt.save() cls.set_number(receipts) write_log('log_action.msg_confirmed', receipts) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, receipts): Move = Pool().get('account.move') for receipt in receipts: for line in receipt.lines: line.reconcile() Move.post([r.move for r in receipts]) write_log('log_action.msg_posted', receipts) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, receipts): Move = Pool().get('account.move') Move.delete([r.move for r in receipts]) write_log('log_action.msg_cancelled', receipts)
class Author(ModelSQL, ModelView): 'Author' __name__ = 'library.author' books = fields.One2Many('library.book', 'author', 'Books') name = fields.Char('Name', required=True) birth_date = fields.Date( 'Birth date', states={'required': Bool(Eval('death_date', 'False'))}, depends=['death_date']) death_date = fields.Date('Death date', domain=[ 'OR', ('death_date', '=', None), ('death_date', '>', Eval('birth_date')) ], states={'invisible': ~Eval('birth_date')}, depends=['birth_date']) gender = fields.Selection([('man', 'Man'), ('woman', 'Woman')], 'Gender') age = fields.Function( fields.Integer('Age', states={'invisible': ~Eval('death_date')}), 'on_change_with_age') number_of_books = fields.Function(fields.Integer('Number of books'), 'getter_number_of_books') genres = fields.Function(fields.Many2Many( 'library.genre', None, None, 'Genres', states={'invisible': ~Eval('books', False)}), 'getter_genres', searcher='searcher_genres') latest_book = fields.Function( fields.Many2One('library.book', 'Latest Book', states={'invisible': ~Eval('books', False)}), 'getter_latest_book') @fields.depends('birth_date') def on_change_birth_date(self): if not self.birth_date: self.death_date = None @fields.depends('books') def on_change_books(self): if not self.books: self.genres = [] self.number_of_books = 0 return self.number_of_books, genres = 0, set() for book in self.books: self.number_of_books += 1 if book.genre: genres.add(book.genre) self.genres = list(genres) @fields.depends('birth_date', 'death_date') def on_change_with_age(self, name=None): if not self.birth_date: return None end_date = self.death_date or datetime.date.today() age = end_date.year - self.birth_date.year if (end_date.month, end_date.day) < (self.birth_date.month, self.birth_date.day): age -= 1 return age def getter_genres(self, name): genres = set() for book in self.books: if book.genre: genres.add(book.genre.id) return list(genres) @classmethod def getter_latest_book(cls, authors, name): result = {x.id: None for x in authors} Book = Pool().get('library.book') book = Book.__table__() sub_book = Book.__table__() cursor = Transaction().connection.cursor() sub_query = sub_book.select( sub_book.author, Max(Coalesce(sub_book.publishing_date, datetime.date.min), window=Window([sub_book.author])).as_('max_date'), where=sub_book.author.in_([x.id for x in authors])) cursor.execute( *book.join(sub_query, condition=(book.author == sub_query.author) & (Coalesce(book.publishing_date, datetime.date.min) == sub_query.max_date)).select(book.author, book.id)) for author_id, book in cursor.fetchall(): result[author_id] = book return result @classmethod def getter_number_of_books(cls, authors, name): result = {x.id: 0 for x in authors} Book = Pool().get('library.book') book = Book.__table__() cursor = Transaction().connection.cursor() cursor.execute( *book.select(book.author, Count(book.id), where=book.author.in_([x.id for x in authors]), group_by=[book.author])) for author_id, count in cursor.fetchall(): result[author_id] = count return result @classmethod def searcher_genres(cls, name, clause): return []
class BoardLaboratory(metaclass=PoolMeta): __name__ = 'lims.board.laboratory' analysis_sheet_templates = fields.One2Many( 'lims.board.laboratory.analysis_sheet_template', None, 'Unplanned samples per Analysis sheet', states={'readonly': True}) @ModelView.button_change('analysis_sheet_templates') def apply_filter(self): super().apply_filter() self.analysis_sheet_templates = self.get_analysis_sheet_templates() def get_analysis_sheet_templates(self): pool = Pool() TemplateAnalysisSheet = pool.get('lims.template.analysis_sheet') records = [] with Transaction().set_context( date_from=self.date_from, date_to=self.date_to): templates = TemplateAnalysisSheet.browse(self._get_templates()) for t in templates: record = {'t': t.name, 'q': t.pending_fractions} records.append(record) return records def _get_templates(self): cursor = Transaction().connection.cursor() pool = Pool() PlanificationServiceDetail = pool.get( 'lims.planification.service_detail') PlanificationDetail = pool.get('lims.planification.detail') Planification = pool.get('lims.planification') NotebookLine = pool.get('lims.notebook.line') Notebook = pool.get('lims.notebook') Fraction = pool.get('lims.fraction') EntryDetailAnalysis = pool.get('lims.entry.detail.analysis') Analysis = pool.get('lims.analysis') Template = pool.get('lims.template.analysis_sheet') TemplateAnalysis = pool.get('lims.template.analysis_sheet.analysis') cursor.execute('SELECT nl.id ' 'FROM "' + NotebookLine._table + '" nl ' 'INNER JOIN "' + PlanificationServiceDetail._table + '" psd ON psd.notebook_line = nl.id ' 'INNER JOIN "' + PlanificationDetail._table + '" pd ' 'ON psd.detail = pd.id ' 'INNER JOIN "' + Planification._table + '" p ' 'ON pd.planification = p.id ' 'WHERE p.state = \'preplanned\'') planned_lines = [x[0] for x in cursor.fetchall()] planned_lines_ids = ', '.join(str(x) for x in [0] + planned_lines) preplanned_where = 'AND nl.id NOT IN (%s) ' % planned_lines_ids dates_where = '' if self.date_from: dates_where += ('AND ad.confirmation_date::date >= \'%s\'::date ' % self.date_from) if self.date_to: dates_where += ('AND ad.confirmation_date::date <= \'%s\'::date ' % self.date_to) sql_select = 'SELECT nl.analysis, nl.method ' sql_from = ( 'FROM "' + NotebookLine._table + '" nl ' 'INNER JOIN "' + Analysis._table + '" nla ' 'ON nla.id = nl.analysis ' 'INNER JOIN "' + Notebook._table + '" nb ' 'ON nb.id = nl.notebook ' 'INNER JOIN "' + Fraction._table + '" frc ' 'ON frc.id = nb.fraction ' 'INNER JOIN "' + EntryDetailAnalysis._table + '" ad ' 'ON ad.id = nl.analysis_detail ') sql_where = ( 'WHERE ad.plannable = TRUE ' 'AND nl.start_date IS NULL ' 'AND nl.annulled = FALSE ' 'AND nla.behavior != \'internal_relation\' ' + preplanned_where + dates_where) with Transaction().set_user(0): cursor.execute(sql_select + sql_from + sql_where) notebook_lines = cursor.fetchall() if not notebook_lines: return [] result = [] for nl in notebook_lines: cursor.execute('SELECT t.id ' 'FROM "' + Template._table + '" t ' 'INNER JOIN "' + TemplateAnalysis._table + '" ta ' 'ON t.id = ta.template ' 'WHERE t.active IS TRUE ' 'AND ta.analysis = %s ' 'AND (ta.method = %s OR ta.method IS NULL)', (nl[0], nl[1])) template = cursor.fetchone() if template: result.append(template[0]) return list(set(result))
class Module(ModelSQL, ModelView): "Module" __name__ = "ir.module" name = fields.Char("Name", readonly=True, required=True) version = fields.Function(fields.Char('Version'), 'get_version') dependencies = fields.One2Many('ir.module.dependency', 'module', 'Dependencies', readonly=True) parents = fields.Function(fields.One2Many('ir.module', None, 'Parents'), 'get_parents') childs = fields.Function(fields.One2Many('ir.module', None, 'Childs'), 'get_childs') state = fields.Selection([ ('not activated', 'Not Activated'), ('activated', 'Activated'), ('to upgrade', 'To be upgraded'), ('to remove', 'To be removed'), ('to activate', 'To be activated'), ], string='State', readonly=True) @classmethod def __setup__(cls): super(Module, cls).__setup__() table = cls.__table__() cls._sql_constraints = [ ('name_uniq', Unique(table, table.name), 'The name of the module must be unique!'), ] cls._order.insert(0, ('name', 'ASC')) cls.__rpc__.update({ 'on_write': RPC(instantiate=0), }) cls._buttons.update({ 'activate': { 'invisible': Eval('state') != 'not activated', 'depends': ['state'], }, 'activate_cancel': { 'invisible': Eval('state') != 'to activate', 'depends': ['state'], }, 'deactivate': { 'invisible': Eval('state') != 'activated', 'depends': ['state'], }, 'deactivate_cancel': { 'invisible': Eval('state') != 'to remove', 'depends': ['state'], }, 'upgrade': { 'invisible': Eval('state') != 'activated', 'depends': ['state'], }, 'upgrade_cancel': { 'invisible': Eval('state') != 'to upgrade', 'depends': ['state'], }, }) @classmethod def __register__(cls, module_name): pool = Pool() ModelData = pool.get('ir.model.data') sql_table = cls.__table__() model_data_sql_table = ModelData.__table__() cursor = Transaction().connection.cursor() # Migration from 3.6: remove double module old_table = 'ir_module_module' if backend.TableHandler.table_exist(old_table): backend.TableHandler.table_rename(old_table, cls._table) super(Module, cls).__register__(module_name) # Migration from 4.0: rename installed to activated cursor.execute(*sql_table.update([sql_table.state], ['activated'], where=sql_table.state == 'installed')) cursor.execute( *sql_table.update([sql_table.state], ['not activated'], where=sql_table.state == 'uninstalled')) # Migration from 4.6: register buttons on ir module button_fs_ids = [ 'module_activate_button', 'module_activate_cancel_button', 'module_deactivate_button', 'module_deactivate_cancel_button', 'module_upgrade_button', 'module_upgrade_cancel_button', ] cursor.execute(*model_data_sql_table.update( [model_data_sql_table.module], ['ir'], where=((model_data_sql_table.module == 'res') & (model_data_sql_table.fs_id.in_(button_fs_ids))))) @staticmethod def default_state(): return 'not activated' def get_version(self, name): return get_module_info(self.name).get('version', '') @classmethod def get_parents(cls, modules, name): parent_names = list( set(d.name for m in modules for d in m.dependencies)) parents = cls.search([ ('name', 'in', parent_names), ]) name2id = dict((m.name, m.id) for m in parents) return dict( (m.id, [name2id[d.name] for d in m.dependencies]) for m in modules) @classmethod def get_childs(cls, modules, name): child_ids = dict((m.id, []) for m in modules) name2id = dict((m.name, m.id) for m in modules) childs = cls.search([ ('dependencies.name', 'in', list(name2id.keys())), ]) for child in childs: for dep in child.dependencies: if dep.name in name2id: child_ids[name2id[dep.name]].append(child.id) return child_ids @classmethod def delete(cls, records): for module in records: if module.state in ( 'activated', 'to upgrade', 'to remove', 'to activate', ): raise AccessError(gettext('ir.msg_module_delete_state')) return super(Module, cls).delete(records) @classmethod def on_write(cls, modules): dependencies = set() def get_parents(module): parents = set(p.id for p in module.parents) for p in module.parents: parents.update(get_parents(p)) return parents def get_childs(module): childs = set(c.id for c in module.childs) for c in module.childs: childs.update(get_childs(c)) return childs for module in modules: dependencies.update(get_parents(module)) dependencies.update(get_childs(module)) return list(dependencies) @classmethod @ModelView.button @filter_state('not activated') def activate(cls, modules): modules_activated = set(modules) def get_parents(module): parents = set(p for p in module.parents) for p in module.parents: parents.update(get_parents(p)) return parents for module in modules: modules_activated.update( (m for m in get_parents(module) if m.state == 'not activated')) cls.write(list(modules_activated), { 'state': 'to activate', }) @classmethod @ModelView.button @filter_state('activated') def upgrade(cls, modules): modules_activated = set(modules) def get_childs(module): childs = set(c for c in module.childs) for c in module.childs: childs.update(get_childs(c)) return childs for module in modules: modules_activated.update( (m for m in get_childs(module) if m.state == 'activated')) cls.write(list(modules_activated), { 'state': 'to upgrade', }) @classmethod @ModelView.button @filter_state('to activate') def activate_cancel(cls, modules): cls.write(modules, { 'state': 'not activated', }) @classmethod @ModelView.button @filter_state('activated') def deactivate(cls, modules): pool = Pool() Module = pool.get('ir.module') Dependency = pool.get('ir.module.dependency') module_table = Module.__table__() dep_table = Dependency.__table__() cursor = Transaction().connection.cursor() for module in modules: cursor.execute(*dep_table.join( module_table, condition=(dep_table.module == module_table.id) ).select( module_table.state, module_table.name, where=(dep_table.name == module.name) & NotIn(module_table.state, ['not activated', 'to remove']))) res = cursor.fetchall() if res: raise DeactivateDependencyError( gettext('ir.msg_module_deactivate_dependency'), '\n'.join('\t%s: %s' % (x[0], x[1]) for x in res)) cls.write(modules, {'state': 'to remove'}) @classmethod @ModelView.button @filter_state('to remove') def deactivate_cancel(cls, modules): cls.write(modules, {'state': 'not activated'}) @classmethod @ModelView.button @filter_state('to upgrade') def upgrade_cancel(cls, modules): cls.write(modules, {'state': 'activated'}) @classmethod def update_list(cls): 'Update the list of available packages' count = 0 module_names = get_module_list() modules = cls.search([]) name2module = dict((m.name, m) for m in modules) cls.delete([ m for m in modules if m.state != 'activated' and m.name not in module_names ]) # iterate through activated modules and mark them as being so for name in module_names: if name in name2module: module = name2module[name] tryton = get_module_info(name) cls._update_dependencies(module, tryton.get('depends', [])) continue tryton = get_module_info(name) if not tryton: continue module, = cls.create([{ 'name': name, 'state': 'not activated', }]) count += 1 cls._update_dependencies(module, tryton.get('depends', [])) return count @classmethod def _update_dependencies(cls, module, depends=None): pool = Pool() Dependency = pool.get('ir.module.dependency') Dependency.delete( [x for x in module.dependencies if x.name not in depends]) if depends is None: depends = [] # Restart Browse Cache for deleted dependencies module = cls(module.id) dependency_names = [x.name for x in module.dependencies] to_create = [] for depend in depends: if depend not in dependency_names: to_create.append({ 'module': module.id, 'name': depend, }) if to_create: Dependency.create(to_create)
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') to_reconcile = fields.Function(fields.Boolean("To Reconcile"), 'get_to_reconcile') @classmethod def __setup__(cls): super(Statement, cls).__setup__() cls._order[0] = ('id', 'DESC') cls._transitions |= set(( ('draft', 'validated'), ('draft', 'cancel'), ('validated', 'posted'), ('validated', 'cancel'), ('cancel', 'draft'), )) cls._buttons.update({ 'draft': { 'invisible': Eval('state') != 'cancel', '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', 'cancel']), '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)) @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('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 (getattr(line, 'invoice', None) and line.invoice.currency == self.company.currency): invoices.add(line.invoice) for origin in self.origins: for line in origin.lines: if (getattr(line, 'invoice', None) 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 (getattr(line, 'invoice', None) 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 (getattr(line, 'invoice', None) 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 getattr(line, 'invoice', None) 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 [ ('/tree', 'visual', If(Eval('state') == 'cancel', 'muted', '')), ] @classmethod def delete(cls, statements): # Cancel before delete cls.cancel(statements) for statement in statements: if statement.state != 'cancel': 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') 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) if Warning.check(warning_key): raise StatementValidateWarning( warning_key, gettext('account_statement' '.msg_statement_paid_invoice_draft')) 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]) 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)) 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) @classmethod @ModelView.button_action('account_statement.act_reconcile') def reconcile(cls, statements): pass
class Work: __metaclass__ = PoolMeta __name__ = 'project.work' project_invoice_method = fields.Selection(INVOICE_METHODS, 'Invoice Method', states={ 'readonly': Bool(Eval('invoiced_duration')), 'required': Eval('type') == 'project', 'invisible': Eval('type') != 'project', }, depends=['invoiced_duration', 'type']) invoice_method = fields.Function(fields.Selection(INVOICE_METHODS, 'Invoice Method'), 'get_invoice_method') invoiced_duration = fields.Function(fields.TimeDelta('Invoiced Duration', 'company_work_time', states={ 'invisible': Eval('invoice_method') == 'manual', }, depends=['invoice_method']), 'get_total') duration_to_invoice = fields.Function(fields.TimeDelta( 'Duration to Invoice', 'company_work_time', states={ 'invisible': Eval('invoice_method') == 'manual', }, depends=['invoice_method']), 'get_total') invoiced_amount = fields.Function(fields.Numeric('Invoiced Amount', digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('invoice_method') == 'manual', }, depends=['currency_digits', 'invoice_method']), 'get_total') invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', readonly=True) invoiced_progress = fields.One2Many('project.work.invoiced_progress', 'work', 'Invoiced Progress', readonly=True) @classmethod def __setup__(cls): super(Work, cls).__setup__() cls._buttons.update({ 'invoice': { 'invisible': ((Eval('type') != 'project') | (Eval('project_invoice_method', 'manual') == 'manual')), 'readonly': ~Eval('duration_to_invoice'), }, }) cls._error_messages.update({ 'missing_product': 'There is no product on work "%s".', 'missing_list_price': 'There is no list price on work "%s".', 'missing_party': 'There is no party on work "%s".', }) @staticmethod def default_project_invoice_method(): return 'manual' @classmethod def copy(cls, records, default=None): if default is None: default = {} default = default.copy() default.setdefault('invoice_line', None) return super(Work, cls).copy(records, default=default) def get_invoice_method(self, name): if self.type == 'project': return self.project_invoice_method elif self.parent: return self.parent.invoice_method else: return 'manual' @staticmethod def default_invoiced_duration(): return datetime.timedelta() @staticmethod def _get_invoiced_duration_manual(works): return {} @staticmethod def _get_invoiced_duration_effort(works): return dict((w.id, w.effort_duration) for w in works if w.invoice_line and w.effort_duration) @staticmethod def _get_invoiced_duration_progress(works): durations = {} for work in works: durations[work.id] = sum((p.effort_duration for p in work.invoiced_progress if p.effort_duration), datetime.timedelta()) return durations @classmethod def _get_invoiced_duration_timesheet(cls, works): return cls._get_duration_timesheet(works, True) @staticmethod def default_duration_to_invoice(): return datetime.timedelta() @staticmethod def _get_duration_to_invoice_manual(works): return {} @staticmethod def _get_duration_to_invoice_effort(works): return dict((w.id, w.effort_duration) for w in works if w.state == 'done' and not w.invoice_line) @staticmethod def _get_duration_to_invoice_progress(works): durations = {} for work in works: if work.progress is None or work.effort_duration is None: continue effort_to_invoice = datetime.timedelta( hours=work.effort_hours * work.progress) effort_invoiced = sum( (p.effort_duration for p in work.invoiced_progress), datetime.timedelta()) if effort_to_invoice > effort_invoiced: durations[work.id] = effort_to_invoice - effort_invoiced else: durations[work.id] = datetime.timedelta() return durations @classmethod def _get_duration_to_invoice_timesheet(cls, works): return cls._get_duration_timesheet(works, False) @staticmethod def default_invoiced_amount(): return Decimal(0) @staticmethod def _get_invoiced_amount_manual(works): return {} @staticmethod def _get_invoiced_amount_effort(works): pool = Pool() InvoiceLine = pool.get('account.invoice.line') Currency = pool.get('currency.currency') invoice_lines = InvoiceLine.browse([ w.invoice_line.id for w in works if w.invoice_line]) id2invoice_lines = dict((l.id, l) for l in invoice_lines) amounts = {} for work in works: currency = work.company.currency if work.invoice_line: invoice_line = id2invoice_lines[work.invoice_line.id] invoice_currency = (invoice_line.invoice.currency if invoice_line.invoice else invoice_line.currency) amounts[work.id] = Currency.compute(invoice_currency, Decimal(str(work.effort_hours)) * invoice_line.unit_price, currency) else: amounts[work.id] = Decimal(0) return amounts @classmethod def _get_invoiced_amount_progress(cls, works): pool = Pool() Progress = pool.get('project.work.invoiced_progress') InvoiceLine = pool.get('account.invoice.line') Company = pool.get('company.company') Currency = pool.get('currency.currency') cursor = Transaction().connection.cursor() table = cls.__table__() progress = Progress.__table__() invoice_line = InvoiceLine.__table__() company = Company.__table__() amounts = defaultdict(Decimal) work2currency = {} work_ids = [w.id for w in works] for sub_ids in grouped_slice(work_ids): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(progress, condition=progress.work == table.id ).join(invoice_line, condition=progress.invoice_line == invoice_line.id ).select(table.id, Sum(progress.effort_duration * invoice_line.unit_price), where=where, group_by=table.id)) for work_id, amount in cursor.fetchall(): if isinstance(amount, datetime.timedelta): amount = amount.total_seconds() # Amount computed in second instead of hours if amount is not None: amount /= 60 * 60 else: amount = 0 amounts[work_id] = amount 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.itervalues())) id2currency = {c.id: c for c in currencies} for work in works: currency = id2currency[work2currency[work.id]] amounts[work.id] = currency.round(Decimal(amounts[work.id])) return amounts @classmethod def _get_invoiced_amount_timesheet(cls, works): pool = Pool() TimesheetWork = pool.get('timesheet.work') TimesheetLine = pool.get('timesheet.line') InvoiceLine = pool.get('account.invoice.line') Company = pool.get('company.company') Currency = pool.get('currency.currency') cursor = Transaction().connection.cursor() table = cls.__table__() timesheet_work = TimesheetWork.__table__() timesheet_line = TimesheetLine.__table__() invoice_line = InvoiceLine.__table__() company = Company.__table__() amounts = {} work2currency = {} work_ids = [w.id for w in works] for sub_ids in grouped_slice(work_ids): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(timesheet_work, condition=table.work == timesheet_work.id ).join(timesheet_line, condition=timesheet_line.work == timesheet_work.id ).join(invoice_line, condition=timesheet_line.invoice_line == invoice_line.id ).select(table.id, Sum(timesheet_line.duration * invoice_line.unit_price), where=where, group_by=table.id)) amounts.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.itervalues())) id2currency = {c.id: c for c in currencies} for work in works: currency = id2currency[work2currency[work.id]] amount = amounts.get(work.id, 0) if isinstance(amount, datetime.timedelta): amount = amount.total_seconds() amount = amount / 60 / 60 amounts[work.id] = currency.round(Decimal(str(amount))) return amounts @staticmethod def _get_duration_timesheet(works, invoiced): pool = Pool() TimesheetLine = pool.get('timesheet.line') cursor = Transaction().connection.cursor() line = TimesheetLine.__table__() durations = {} twork2work = dict((w.work.id, w.id) for w in works if w.work) ids = twork2work.keys() for sub_ids in grouped_slice(ids): red_sql = reduce_ids(line.work, sub_ids) if invoiced: where = line.invoice_line != Null else: where = line.invoice_line == Null cursor.execute(*line.select(line.work, Sum(line.duration), where=red_sql & where, group_by=line.work)) for twork_id, duration in cursor.fetchall(): if duration: # SQLite uses float for SUM if not isinstance(duration, datetime.timedelta): duration = datetime.timedelta(seconds=duration) durations[twork2work[twork_id]] = duration return durations @classmethod def _get_invoice_values(cls, works, name): default = getattr(cls, 'default_%s' % name) durations = dict.fromkeys((w.id for w in works), default()) method2works = defaultdict(list) for work in works: method2works[work.invoice_method].append(work) for method, m_works in method2works.iteritems(): method = getattr(cls, '_get_%s_%s' % (name, method)) # Re-browse for cache alignment durations.update(method(cls.browse(m_works))) return durations @classmethod def _get_invoiced_duration(cls, works): return cls._get_invoice_values(works, 'invoiced_duration') @classmethod def _get_duration_to_invoice(cls, works): return cls._get_invoice_values(works, 'duration_to_invoice') @classmethod def _get_invoiced_amount(cls, works): return cls._get_invoice_values(works, 'invoiced_amount') @classmethod @ModelView.button def invoice(cls, works): pool = Pool() Invoice = pool.get('account.invoice') invoices = [] for work in works: invoice_lines = work._get_lines_to_invoice() if not invoice_lines: continue invoice = work._get_invoice() invoice.save() invoices.append(invoice) for key, lines in groupby(invoice_lines, key=work._group_lines_to_invoice_key): lines = list(lines) key = dict(key) invoice_line = work._get_invoice_line(key, invoice, lines) invoice_line.invoice = invoice.id invoice_line.save() origins = {} for line in lines: origin = line['origin'] origins.setdefault(origin.__class__, []).append(origin) for klass, records in origins.iteritems(): klass.save(records) # Store first new origins klass.write(records, { 'invoice_line': invoice_line.id, }) Invoice.update_taxes(invoices) def _get_invoice(self): "Return invoice for the work" pool = Pool() Invoice = pool.get('account.invoice') Journal = pool.get('account.journal') journals = Journal.search([ ('type', '=', 'revenue'), ], limit=1) if journals: journal, = journals else: journal = None if not self.party: self.raise_user_error('missing_party', (self.rec_name,)) return Invoice( company=self.company, type='out', journal=journal, party=self.party, invoice_address=self.party.address_get(type='invoice'), currency=self.company.currency, account=self.party.account_receivable, payment_term=self.party.customer_payment_term, description=self.name, ) def _group_lines_to_invoice_key(self, line): "The key to group lines" return (('product', line['product']), ('unit_price', line['unit_price']), ('description', line['description'])) def _get_invoice_line(self, key, invoice, lines): "Return a invoice line for the lines" pool = Pool() InvoiceLine = pool.get('account.invoice.line') ModelData = pool.get('ir.model.data') Uom = pool.get('product.uom') hour = Uom(ModelData.get_id('product', 'uom_hour')) quantity = sum(l['quantity'] for l in lines) product = key['product'] invoice_line = InvoiceLine() invoice_line.type = 'line' invoice_line.quantity = Uom.compute_qty(hour, quantity, product.default_uom) invoice_line.unit = product.default_uom invoice_line.product = product invoice_line.description = key['description'] invoice_line.account = product.account_revenue_used invoice_line.unit_price = Uom.compute_price(hour, key['unit_price'], product.default_uom) taxes = [] pattern = invoice_line._get_tax_rule_pattern() party = invoice.party for tax in product.customer_taxes_used: if party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax.id) if party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) invoice_line.taxes = taxes return invoice_line def _get_lines_to_invoice_manual(self): return [] def _get_lines_to_invoice_effort(self): if (not self.invoice_line and self.effort_hours and self.state == 'done'): if not self.product: self.raise_user_error('missing_product', (self.rec_name,)) elif self.list_price is None: self.raise_user_error('missing_list_price', (self.rec_name,)) return [{ 'product': self.product, 'quantity': self.effort_hours, 'unit_price': self.list_price, 'origin': self, 'description': self.name, }] return [] def _get_lines_to_invoice_progress(self): pool = Pool() InvoicedProgress = pool.get('project.work.invoiced_progress') ModelData = pool.get('ir.model.data') Uom = pool.get('product.uom') hour = Uom(ModelData.get_id('product', 'uom_hour')) if self.progress is None or self.effort_duration is None: return [] invoiced_progress = sum(x.effort_hours for x in self.invoiced_progress) quantity = self.effort_hours * self.progress - invoiced_progress quantity = Uom.compute_qty(hour, quantity, self.product.default_uom) if quantity > 0: if not self.product: self.raise_user_error('missing_product', (self.rec_name,)) elif self.list_price is None: self.raise_user_error('missing_list_price', (self.rec_name,)) invoiced_progress = InvoicedProgress(work=self, effort_duration=datetime.timedelta(hours=quantity)) return [{ 'product': self.product, 'quantity': quantity, 'unit_price': self.list_price, 'origin': invoiced_progress, 'description': self.name, 'description': self.name, }] return [] def _get_lines_to_invoice_timesheet(self): if self.work and self.work.timesheet_lines: if not self.product: self.raise_user_error('missing_product', (self.rec_name,)) elif self.list_price is None: self.raise_user_error('missing_list_price', (self.rec_name,)) return [{ 'product': self.product, 'quantity': l.hours, 'unit_price': self.list_price, 'origin': l, 'description': self.name, } for l in self.work.timesheet_lines if not l.invoice_line] return [] def _test_group_invoice(self): return (self.company, self.party) def _get_lines_to_invoice(self, test=None): "Return lines for work and children" lines = [] if test is None: test = self._test_group_invoice() lines += getattr(self, '_get_lines_to_invoice_%s' % self.invoice_method)() for children in self.children: if children.type == 'project': if test != children._test_group_invoice(): continue lines += children._get_lines_to_invoice(test=test) return lines
class Origin(origin_mixin(_states, _depends), 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')), ('date', '=', Eval('date')), ], depends=['statement', 'date', 'statement_id']) statement_id = fields.Function(fields.Integer("Statement ID"), 'on_change_with_statement_id') pending_amount = fields.Function(fields.Numeric( "Pending Amount", digits=(16, Eval('_parent_statement', {}).get('currency_digits', 2))), '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)]
class Channel: """ Sale Channel model """ __name__ = 'sale.channel' # Instance magento_url = fields.Char("Magento Site URL", states=MAGENTO_STATES, depends=['source']) magento_api_user = fields.Char("API User", states=MAGENTO_STATES, depends=['source']) magento_api_key = fields.Char("API Key", states=MAGENTO_STATES, depends=['source']) magento_carriers = fields.One2Many("magento.instance.carrier", "channel", "Carriers / Shipping Methods", states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_order_prefix = fields.Char( 'Sale Order Prefix', help="This helps to distinguish between orders from different channels", states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) # website magento_website_id = fields.Integer('Website ID', readonly=True, states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_website_name = fields.Char('Website Name', readonly=True, states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_website_code = fields.Char('Website Code', readonly=True, states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_root_category_id = fields.Integer('Root Category ID', states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_store_name = fields.Char('Store Name', readonly=True, states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_store_id = fields.Integer('Store ID', readonly=True, states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) #: Checking this will make sure that only the done shipments which have a #: carrier and tracking reference are exported. magento_export_tracking_information = fields.Boolean( 'Export tracking information', help='Checking this will make sure' ' that only the done shipments which have a carrier and tracking ' 'reference are exported. This will update carrier and tracking ' 'reference on magento for the exported shipments as well.', states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_taxes = fields.One2Many("sale.channel.magento.tax", "channel", "Taxes", states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) magento_price_tiers = fields.One2Many('sale.channel.magento.price_tier', 'channel', 'Default Price Tiers', states=INVISIBLE_IF_NOT_MAGENTO, depends=['source']) product_listings = fields.One2Many( 'product.product.channel_listing', 'channel', 'Product Listings', ) magento_payment_gateways = fields.One2Many( 'magento.instance.payment_gateway', 'channel', 'Payments', ) @classmethod def __setup__(cls): """ Setup the class before adding to pool """ super(Channel, cls).__setup__() cls._sql_constraints += [ ('unique_magento_channel', 'UNIQUE(magento_url, magento_website_id, magento_store_id)', 'This store is already added') ] cls._error_messages.update({ "connection_error": "Incorrect API Settings! \n" "Please check and correct the API settings on channel.", "multiple_channels": 'Selected operation can be done only for one' ' channel at a time', 'invalid_magento_channel': 'Current channel does not belongs to Magento !' }) cls._buttons.update({ 'import_magento_carriers': { 'invisible': Eval('source') != 'magento' }, 'configure_magento_connection': { 'invisible': Eval('source') != 'magento' } }) def validate_magento_channel(self): """ Make sure channel source is magento """ if self.source != 'magento': self.raise_user_error('invalid_magento_channel') @classmethod def get_source(cls): """ Get the source """ res = super(Channel, cls).get_source() res.append(('magento', 'Magento')) return res @staticmethod def default_magento_order_prefix(): """ Sets default value for magento order prefix """ return 'mag_' @staticmethod def default_magento_root_category_id(): """ Sets default root category id. Is set to 1, because the default root category is 1 """ return 1 def get_taxes(self, rate): "Return list of tax records with the given rate" for mag_tax in self.magento_taxes: if mag_tax.tax_percent == rate: return list(mag_tax.taxes) return [] def import_order_states(self): """ Import order states for magento channel Downstream implementation for channel.import_order_states """ if self.source != 'magento': return super(Channel, self).import_order_states() with Transaction().set_context({'current_channel': self.id}): # Import order states with OrderConfig(self.magento_url, self.magento_api_user, self.magento_api_key) as order_config_api: order_states_data = order_config_api.get_states() for code, name in order_states_data.iteritems(): self.create_order_state(code, name) @classmethod @ModelView.button_action('magento.wizard_configure_magento') def configure_magento_connection(cls, channels): """ Configure magento connection for current channel :param channels: List of active records of channels """ pass def test_magento_connection(self): """ Test magento connection and display appropriate message to user :param channels: Active record list of magento channels """ # Make sure channel belongs to magento self.validate_magento_channel() try: with magento.API(self.magento_url, self.magento_api_user, self.magento_api_key): return except (xmlrpclib.Fault, IOError, xmlrpclib.ProtocolError, socket.timeout): self.raise_user_error("connection_error") @classmethod @ModelView.button_action('magento.wizard_import_magento_carriers') def import_magento_carriers(cls, channels): """ Import carriers/shipping methods from magento for channels :param channels: Active record list of magento channels """ InstanceCarrier = Pool().get('magento.instance.carrier') for channel in channels: channel.validate_magento_channel() with Transaction().set_context({'current_channel': channel.id}): with OrderConfig(channel.magento_url, channel.magento_api_user, channel.magento_api_key) as order_config_api: mag_carriers = order_config_api.get_shipping_methods() InstanceCarrier.create_all_using_magento_data(mag_carriers) @classmethod def get_current_magento_channel(cls): """Helper method to get the current magento_channel. """ channel = cls.get_current_channel() # Make sure channel belongs to magento channel.validate_magento_channel() return channel def import_products(self): """ Import products for this magento channel Downstream implementation for channel.import_products """ if self.source != 'magento': return super(Channel, self).import_products() self.import_category_tree() with Transaction().set_context({'current_channel': self.id}): with magento.Product(self.magento_url, self.magento_api_user, self.magento_api_key) as product_api: # TODO: Implement pagination and import each product as async # task magento_products = product_api.list() products = [] for magento_product in magento_products: products.append(self.import_product( magento_product['sku'])) return products def import_product(self, sku): """ Import specific product for this magento channel Downstream implementation for channel.import_product """ Product = Pool().get('product.product') Listing = Pool().get('product.product.channel_listing') if self.source != 'magento': return super(Channel, self).import_product(sku) # Sanitize SKU sku = sku.strip() products = Product.search([ ('code', '=', sku), ]) listings = Listing.search([('product.code', '=', sku), ('channel', '=', self)]) if not products or not listings: # Either way we need the product data from magento. Make that # dreaded API call. with magento.Product(self.magento_url, self.magento_api_user, self.magento_api_key) as product_api: product_data = product_api.info(sku, identifierType="SKU") # XXX: sanitize product_data, sometimes product sku may # contain trailing spaces product_data['sku'] = product_data['sku'].strip() # Create a product since there is no match for an existing # product with the SKU. if not products: product = Product.create_from(self, product_data) else: product, = products if not listings: Listing.create_from(self, product_data) else: product = products[0] return product def import_category_tree(self): """ Imports the category tree and creates categories in a hierarchy same as that on Magento :param website: Active record of website """ Category = Pool().get('product.category') self.validate_magento_channel() with Transaction().set_context({'current_channel': self.id}): with magento.Category(self.magento_url, self.magento_api_user, self.magento_api_key) as category_api: category_tree = category_api.tree( self.magento_root_category_id) Category.create_tree_using_magento_data(category_tree) def import_orders(self): """ Downstream implementation of channel.import_orders :return: List of active record of sale imported """ if self.source != 'magento': return super(Channel, self).import_orders() new_sales = [] with Transaction().set_context({'current_channel': self.id}): order_states = self.get_order_states_to_import() order_states_to_import_in = map(lambda state: state.code, order_states) with magento.Order(self.magento_url, self.magento_api_user, self.magento_api_key) as order_api: # Filter orders with date and store_id using list() # then get info of each order using info() # and call find_or_create_using_magento_data on sale filter = { 'store_id': { '=': self.magento_store_id }, 'state': { 'in': order_states_to_import_in }, } if self.last_order_import_time: last_order_import_time = \ self.last_order_import_time.replace( microsecond=0 ) filter.update({ 'updated_at': { 'gteq': last_order_import_time.isoformat(' ') }, }) self.write([self], {'last_order_import_time': datetime.utcnow()}) page = 1 has_next = True orders_summaries = [] while has_next: # XXX: Pagination is only available in # magento extension >= 1.6.1 api_res = order_api.search(filters=filter, limit=3000, page=page) has_next = api_res['hasNext'] page += 1 orders_summaries.extend(api_res['items']) for order_summary in orders_summaries: new_sales.append(self.import_order(order_summary)) return new_sales def import_order(self, order_info): "Downstream implementation to import sale order from magento" if self.source != 'magento': return super(Channel, self).import_order(order_info) Sale = Pool().get('sale.sale') sale = Sale.find_using_magento_data(order_info) if sale: return sale with Transaction().set_context({'current_channel': self.id}): with magento.Order(self.magento_url, self.magento_api_user, self.magento_api_key) as order_api: order_data = order_api.info(order_info['increment_id']) return Sale.create_using_magento_data(order_data) @classmethod def export_order_status_to_magento_using_cron(cls): """ Export sales orders status to magento using cron :param store_views: List of active record of store view """ channels = cls.search([('source', '=', 'magento')]) for channel in channels: channel.export_order_status_to_magento() def export_order_status_to_magento(self): """ Export sale orders to magento for the current store view. If last export time is defined, export only those orders which are updated after last export time. :return: List of active records of sales exported """ Sale = Pool().get('sale.sale') self.validate_magento_channel() exported_sales = [] domain = [('channel', '=', self.id)] if self.last_order_export_time: domain = [('write_date', '>=', self.last_order_export_time)] sales = Sale.search(domain) self.last_order_export_time = datetime.utcnow() self.save() for sale in sales: exported_sales.append(sale.export_order_status_to_magento()) return exported_sales @classmethod def export_shipment_status_to_magento_using_cron(cls): """ Export Shipment status for shipments using cron """ channels = cls.search([('source', '=', 'magento')]) for channel in channels: channel.export_shipment_status_to_magento() def export_shipment_status_to_magento(self): """ Exports shipment status for shipments to magento, if they are shipped :return: List of active record of shipment """ Shipment = Pool().get('stock.shipment.out') Sale = Pool().get('sale.sale') SaleLine = Pool().get('sale.line') self.validate_magento_channel() sale_domain = [ ('channel', '=', self.id), ('shipment_state', '=', 'sent'), ('magento_id', '!=', None), ('shipments', '!=', None), ] if self.last_shipment_export_time: sale_domain.append( ('write_date', '>=', self.last_shipment_export_time)) sales = Sale.search(sale_domain) self.last_shipment_export_time = datetime.utcnow() self.save() updated_sales = set([]) for sale in sales: # Get the increment id from the sale reference increment_id = sale.reference[len(self.magento_order_prefix ):len(sale.reference)] for shipment in sale.shipments: try: # Some checks to make sure that only valid shipments are # being exported if shipment.is_tracking_exported_to_magento or \ shipment.state != 'done' or \ shipment.magento_increment_id: continue updated_sales.add(sale) with magento.Shipment( self.magento_url, self.magento_api_user, self.magento_api_key) as shipment_api: item_qty_map = {} for move in shipment.outgoing_moves: if isinstance(move.origin, SaleLine) \ and move.origin.magento_id: # This is done because there can be multiple # lines with the same product and they need # to be send as a sum of quanitities item_qty_map.setdefault( str(move.origin.magento_id), 0) item_qty_map[str(move.origin.magento_id)] += \ move.quantity shipment_increment_id = shipment_api.create( order_increment_id=increment_id, items_qty=item_qty_map) Shipment.write( list(sale.shipments), { 'magento_increment_id': shipment_increment_id, }) if self.magento_export_tracking_information and ( hasattr(shipment, 'tracking_number') and hasattr(shipment, 'carrier') and shipment.tracking_number and shipment.carrier): with Transaction().set_context( current_channel=self.id): shipment.export_tracking_info_to_magento() except xmlrpclib.Fault, fault: if fault.faultCode == 102: # A shipment already exists for this order, # we cannot do anything about it. # Maybe it was already exported earlier or was created # separately on magento # Hence, just continue continue return updated_sales
class Template(ModelSQL, ModelView): "Product Template" _name = "product.template" _description = __doc__ name = fields.Char('Name', size=None, required=True, translate=True, select=1, states=STATES) type = fields.Selection(_TYPE_PRODUCT, 'Type', required=True, states=STATES) category = fields.Many2One('product.category', 'Category', required=True, states=STATES) group = fields.Many2Many('product.grouping', 'product', 'group', 'Group', states=STATES) default_uom = fields.Many2One('product.uom', 'Default UOM', required=True, states=STATES) active = fields.Boolean('Active', select=1) products = fields.One2Many('product.product', 'template', 'Products', states=STATES) analogue = fields.Many2Many('ekd.product.analogue', 'original', 'product', 'Product Analogue', select=1) product_template = fields.Many2One( 'ekd.product.template', 'Product Template', on_change=['product_template', 'properties', 'description']) properties = fields.One2Many('ekd.product.property', 'product', 'Property', select=2) as_material = fields.Boolean('As Material', select=1) as_fixed = fields.Boolean('As Fixed Assets', select=1) as_intangible = fields.Boolean('As Intangible Assets', select=1) as_goods = fields.Boolean('As Goods', select=1) def default_active(self): return True def default_type(self): return 'stockable' def default_cost_price_method(self): return 'fixed' def get_price_uom(self, ids, name): product_uom_obj = self.pool.get('product.uom') res = {} field = name[:-4] context = Transaction().context if context.get('uom'): to_uom = self.pool.get('product.uom').browse( Transaction().context['uom']) for product in self.browse(ids): res[product.id] = product_uom_obj.compute_price( product.default_uom, product[field], to_uom) else: for product in self.browse(ids): res[product.id] = product[field] return res def copy(self, ids, default=None): if default is None: default = {} default = default.copy() default['products'] = False return super(Template, self).copy(ids, default=default)
class PatientRounding(Workflow, ModelSQL, ModelView): 'Patient Ambulatory Care' __name__ = 'gnuhealth.patient.rounding' hospitalization_location = fields.Many2One( 'stock.location', 'Hospitalization Location', domain=[('type', '=', 'storage')], states={ 'required': If(Or(Bool(Eval('medicaments')), Bool(Eval('medical_supplies'))), True, False), 'readonly': Eval('state') == 'done', }, depends=_DEPENDS) medicaments = fields.One2Many('gnuhealth.patient.rounding.medicament', 'name', 'Medicaments', states=_STATES, depends=_DEPENDS) medical_supplies = fields.One2Many( 'gnuhealth.patient.rounding.medical_supply', 'name', 'Medical Supplies', states=_STATES, depends=_DEPENDS) moves = fields.One2Many('stock.move', 'origin', 'Stock Moves', readonly=True) @classmethod def __setup__(cls): super(PatientRounding, cls).__setup__() cls._transitions |= set((('draft', 'done'), )) cls._buttons.update( {'done': { 'invisible': ~Eval('state').in_(['draft']), }}) @classmethod def copy(cls, roundings, default=None): if default is None: default = {} default = default.copy() default['moves'] = None return super(PatientRounding, cls).copy(roundings, default=default) @classmethod @ModelView.button @Workflow.transition('done') def done(cls, roundings): pool = Pool() Patient = pool.get('gnuhealth.patient') HealthProfessional = pool.get('gnuhealth.healthprofessional') signing_hp = HealthProfessional.get_health_professional() lines_to_ship = {} medicaments_to_ship = [] supplies_to_ship = [] for rounding in roundings: patient = Patient(rounding.name.patient.id) for medicament in rounding.medicaments: medicaments_to_ship.append(medicament) for medical_supply in rounding.medical_supplies: supplies_to_ship.append(medical_supply) lines_to_ship['medicaments'] = medicaments_to_ship lines_to_ship['supplies'] = supplies_to_ship cls.create_stock_moves(roundings, lines_to_ship) cls.write(roundings, { 'signed_by': signing_hp, 'evaluation_end': datetime.now() }) @classmethod def create_stock_moves(cls, roundings, lines): pool = Pool() Move = pool.get('stock.move') Date = pool.get('ir.date') moves = [] for rounding in roundings: for medicament in lines['medicaments']: move_info = {} move_info['origin'] = str(rounding) move_info['product'] = medicament.medicament.name.id move_info['uom'] = medicament.medicament.name.default_uom.id move_info['quantity'] = medicament.quantity move_info['from_location'] = \ rounding.hospitalization_location.id move_info['to_location'] = \ rounding.name.patient.name.customer_location.id move_info['unit_price'] = medicament.medicament.name.list_price if medicament.lot: if medicament.lot.expiration_date \ and medicament.lot.expiration_date < Date.today(): raise UserError('Expired medicaments') move_info['lot'] = medicament.lot.id moves.append(move_info) for medical_supply in lines['supplies']: move_info = {} move_info['origin'] = str(rounding) move_info['product'] = medical_supply.product.id move_info['uom'] = medical_supply.product.default_uom.id move_info['quantity'] = medical_supply.quantity move_info['from_location'] = \ rounding.hospitalization_location.id move_info['to_location'] = \ rounding.name.patient.name.customer_location.id move_info['unit_price'] = medical_supply.product.list_price if medical_supply.lot: if medical_supply.lot.expiration_date \ and medical_supply.lot.expiration_date < Date.today(): raise UserError('Expired supplies') move_info['lot'] = medical_supply.lot.id moves.append(move_info) new_moves = Move.create(moves) Move.write(new_moves, { 'state': 'done', 'effective_date': Date.today(), }) return True
class IncomeTaxDeduction(ModelSQL, ModelView): """ Income Tax projections and real outcomes """ __name__ = 'income_tax.deduction' salary_code = fields.Char('Salary Code') employee = fields.Many2One('company.employee', 'Employee') designation = fields.Many2One('employee.designation', 'Designation') department = fields.Many2One('company.department', 'Department') start_date = fields.Date('Start Date') end_date = fields.Date('End Date') fiscal_year = fields.Many2One('account.fiscalyear', 'Fiscal Year') annual_salary_ytd = fields.Float( 'Annual Salary (YTD)' ) annual_salary_projected = fields.Float( 'Annual Salary (Projected)' ) income_from_other_source = fields.Float( 'Income from other Sources' ) annual_taxable_income_ytd = fields.Float( 'Annual Taxable Income (YTD)', help="Annual Taxable Income on which " "we calculate the income tax." ) annual_taxable_income_projected = fields.Float( 'Annual Taxable Income (Projected)', help="Projected Annual Taxable Income on " "which we calculate the income tax." ) income_tax_projected = fields.Float( 'Projected Income Tax', help="This is subjected to variations based on " "the Income Tax Declarations, change in Salary " "or any Govt. Rules or policies") income_tax_ytd = fields.Float( 'Income Tax (Year To Date)', help="This is the actual TDS deducted in " "the current financial year.") tds_lines = fields.One2Many( 'income_tax.taxable_amount_lines', 'taxable_amount_projections', 'Projected Income Taxable Amount Lines' ) # For calculation purpose only - Start projected_tds_lines = fields.One2Many( 'income_tax.taxable_amount_lines', 'taxable_amount_projections', 'Projected Income Taxable Amount Lines', domain=[('state', '=', 'projected')] ) deducted_tds_lines = fields.One2Many( 'income_tax.taxable_amount_lines', 'taxable_amount_projections', 'Deducted Income Taxable Amount Lines', domain=[('state', '=', 'deducted')] ) @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): if self.employee: self.salary_code = self.employee.salary_code self.designation = self.employee.designation self.department = self.employee.department @staticmethod def default_start_date(): ''' returns start_date as this year's current fiscal year's start_date value. ''' pool = Pool() fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) return fiscal(current_fiscal_year).start_date \ if current_fiscal_year else None # @staticmethod # def default_fiscal_year(): # ''' # returns start_date as this year's current # fiscal year's start_date value. # ''' # current_fiscal_year = None # pool = Pool() # fiscal = pool.get('account.fiscalyear') # company = Transaction().context.get('company') # if fiscal.find(company): # current_fiscal_year = fiscal.find(company) # return current_fiscal_year @staticmethod def default_end_date(): ''' returns end_date as this year's current fiscal year's end_date value. ''' pool = Pool() fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) return fiscal(current_fiscal_year).end_date \ if current_fiscal_year else None def get_tax_exemption(self, month, year): '''Get the tax exemption from the current year's investment declaration''' pool = Pool() tax_exemption = 0 current_inv_declaration_1 = None fiscal = pool.get('account.fiscalyear') inv_declaration = pool.get('investment.declaration') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) current_date = datetime.date(year, int(month), 1) current_inv_declaration = inv_declaration.search([ ('employee', '=', self.employee), ('fiscal_year', '=', current_fiscal_year) ], order=[('write_date', 'DESC')]) for declaration in current_inv_declaration: date = declaration.write_date.date() if date <= current_date: current_inv_declaration_1 = declaration break if current_inv_declaration_1: tax_exemption = current_inv_declaration_1.net_tax_exempted return tax_exemption def get_tds_line(self, month, year): '''Returns the dictionary for TDS Lines''' vals = {} for line in self.tds_lines: if line.month == month and line.year == year: vals = { 'tds': line.amount, 'state': line.state, } return vals def calculate_monthly_tds(self): ''' get total taxed amount till date and add projected taxed amount plus the investment declaration, if any based on the tax slab for the remaining months on monthly basis ''' pool = Pool() TaxSlab = pool.get('income_tax.rule') Payslip = pool.get('hr.payslip') PayslipLine = Pool().get('hr.payslip.line') fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) # Get payslips of current fiscal year payslips = Payslip.search([ ('employee', '=', self.employee), ('fiscal_year', '=', current_fiscal_year) ], order=[('year', 'ASC')]) annual_tax = TaxSlab.get_annual_income_tax( self.employee, self.annual_taxable_income_projected ) current_month = datetime.date.today().month current_year = datetime.date.today().year tax_exemption = self.get_tax_exemption( str(current_month), current_year ) # Saving Projected Income Tax for current fiscal year if annual_tax: self.income_tax_projected = annual_tax else: self.raise_user_error('No Income Tax Rule defined') if not self.tds_lines: # If there are no tds lines, then create the new sheet TDSLines = pool.get('income_tax.taxable_amount_lines') if payslips: for payslip in payslips: if int(payslip.month) == current_month: break payslip_lines = PayslipLine.search([ ('payslip', '=', payslip), ('salary_rule.code', '=', 'TDS') ]) payslip_tds = payslip_lines[0] \ if payslip_lines else None TDSLines.create([ { 'month': payslip.month, 'year': payslip.year, 'amount': payslip_tds.amount if payslip_tds else 0, 'state': 'deducted', 'taxable_amount_projections': self } ]) if tax_exemption: monthly_tds = (annual_tax / 12) - (tax_exemption / 12) else: monthly_tds = (annual_tax / 12) if current_month >= 4: for month in range(current_month, 13): TDSLines.create([ { 'month': str(month), 'year': datetime.date.today().year, 'amount': monthly_tds, 'state': 'projected', 'taxable_amount_projections': self } ]) for month in range(current_month if current_month < 4 else 1, 4): TDSLines.create( [ { 'month': str(month), 'year': datetime.date.today().year + 1, 'amount': monthly_tds, 'state': 'projected', 'taxable_amount_projections': self } ] ) else: payslips = Payslip.search([ ('employee', '=', self.employee), ('fiscal_year', '=', self.fiscal_year), ], order=[('year', 'ASC')]) for payslip in payslips: tds_month = 0 exemption = 0 if int(payslip.month) == current_month: break payslip_lines = PayslipLine.search([ ('payslip', '=', payslip), ('salary_rule.code', '=', 'TDS') ]) month = int(payslip.month) payslip_tds = payslip_lines[0] \ if payslip_lines else None if payslip_tds: tds_month = payslip_tds.amount if tds_month == 0: ded = 0 for line in self.tds_lines: if line.state == 'deducted': ded += line.amount else: break remaining_tax = annual_tax - ded exemption = self.get_tax_exemption( payslip.month, payslip.year ) remaining_months = 12 \ if month == 4 else self.get_remaining_months(month) tds_month = remaining_tax / remaining_months \ - exemption / remaining_months payslip_tds.amount = tds_month payslip_tds.save() for line in self.tds_lines: if line.month == payslip.month \ and line.year == payslip.year: line.amount = tds_month if line.state == 'projected': line.state = 'deducted' line.save() break deducted = 0 for line in self.tds_lines: if line.state == 'deducted': deducted += line.amount else: break remaining_tax = annual_tax - deducted remaining_months = self.get_remaining_months(current_month) remaining_monthly_tds = remaining_tax / remaining_months \ - tax_exemption/remaining_months for pline in self.tds_lines: if pline.state == 'projected': pline.amount = remaining_monthly_tds pline.save() # TODO: review the method for calculation button calls twice def calculate_income_tax_ytd(self): ''' TODO: review the docstring ''' pool = Pool() Payslip = pool.get('hr.payslip') PayslipLine = pool.get('hr.payslip.line') payslips = Payslip.search([ ('employee', '=', self.employee), ('fiscal_year', '=', self.fiscal_year), ]) annual_tax_ytd = 0 for payslip in payslips: payslip_tds = PayslipLine.search([ ('payslip', '=', payslip), ('salary_rule.code', '=', 'TDS'), ]) tds = payslip_tds[0].amount if payslip_tds else 0 annual_tax_ytd += tds # Saving Income Tax YTD for current fiscal year self.income_tax_ytd = annual_tax_ytd def get_remaining_months(self, month): ''' calculate the remaining number of months in the current financial year ''' current_month = month if current_month <= 12 and current_month > 3: remaining_months = (12-current_month)+4 # 3 months in the next year and 1 to include the current month return remaining_months elif current_month <= 3: remaining_months = (3-current_month)+1 # 1 to include the current month return remaining_months @classmethod def __setup__(cls): super().__setup__() cls._buttons.update({ 'calculate_income_tax_sheet': {}, }) def calculate_income_from_other_sources(self): '''Get Income from other sources from Investment Decalaration''' ''' Algo - Go to employee > current year investment_decalartion and get the income from other sources ''' employee_investment = [] pool = Pool() Investment = pool.get('investment.declaration') investments = Investment.search( [ ('employee', '=', self.employee), ], order=[('write_date', 'DESC')] ) if investments: employee_investment = investments[0] self.income_from_other_source = \ employee_investment.total_income_declared else: self.income_from_other_source = 0 def calculate_taxable_salary_ytd(self): '''Get taxable salary from the payslips in current fiscal year''' ''' Algo - Go to Employee > Payslips. Search for current year's payslips. You will find the taxable income. ''' pool = Pool() Payslip = pool.get('hr.payslip') fiscal = pool.get('account.fiscalyear') current_month = datetime.date.today().month company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) vals = [ ('employee', '=', self.employee), ('fiscal_year', '=', current_fiscal_year), ] payslips = Payslip.search( vals, order=[('year', 'ASC')] ) res_salary = 0 for payslip in payslips: if int(payslip.month) == current_month: break res_salary += payslip.get_taxable_salary_amount() self.annual_salary_ytd = res_salary self.save() def calculate_taxable_salary_projected(self): ''' Algo - 1. Get the taxable salary from last month's salary slip. 2. Multiply last salary in remaining months 3. Add the YTD salary ''' pool = Pool() vals = [] current_month = datetime.date.today().month Payslip = pool.get('hr.payslip') fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) if current_month == 4: current_year = datetime.date.today().year current_date = datetime.date(current_year, 4, 1) date_prev_year = datetime.date(current_year-1, 4, 1) prev_fiscal_year = fiscal.search([ ('end_date', '<', current_date), ('end_date', '>=', date_prev_year), ]) vals = [ ('employee', '=', self.employee), ('month', '=', str(current_month-1)), ('fiscal_year', '=', prev_fiscal_year), ] else: vals = [ ('employee', '=', self.employee), ('month', '=', str(current_month-1)), ('fiscal_year', '=', current_fiscal_year), ] payslip_prev_month = Payslip.search(vals) global current_payslip res = 0 taxable_salary_prev_month = 0 remaining_months = self.get_remaining_months(current_month) if payslip_prev_month: current_payslip = payslip_prev_month[0] taxable_salary_prev_month = \ current_payslip.get_taxable_salary_amount() res = taxable_salary_prev_month * remaining_months payslips = Payslip.search([ ('employee', '=', self.employee), ('fiscal_year', '=', current_fiscal_year), ], order=[('year', 'ASC')]) res_salary = 0 for payslip in payslips: if int(payslip.month) == current_month: break res_salary += payslip.get_taxable_salary_amount() res_salary_projected = res_salary + res # TODO: Figure out projected salary for month of April self.annual_salary_projected = res_salary_projected def calculate_annual_taxable_income_ytd(self): '''Calculate annual taxable using the following formula: taxable salary from payslips in current fiscal year + income from other source ''' self.annual_taxable_income_ytd = ( self.annual_salary_ytd + self.income_from_other_source) def calculate_annual_taxable_income_projected(self): '''Calculate the projected annual taxable income: ''' self.annual_taxable_income_projected = ( self.annual_salary_projected + self.income_from_other_source) @classmethod def calculate_income_tax_sheet(cls, records): '''Calculate Entire Income Tax Sheet''' for record in records: record.calculate_income_from_other_sources() record.calculate_taxable_salary_ytd() record.calculate_taxable_salary_projected() record.calculate_annual_taxable_income_ytd() record.calculate_annual_taxable_income_projected() record.calculate_income_tax_ytd() record.calculate_monthly_tds() record.save()
class Contract(ModelWorkflow, ModelSQL, ModelView): """Contract Agreement""" _name = 'contract.contract' _description = __doc__ company = fields.Many2One('company.company', 'Company', required=True, states=STATES, select=1, domain=[ ('id', If(In('company', Eval('context', {})), '=', '!='), Get(Eval('context', {}), 'company', 0)), ]) journal = fields.Many2One('account.journal', 'Journal', required=True, states=STATES, domain=[('centralised', '=', False)]) name = fields.Char('Name', required=True, translate=True) description = fields.Char('Description', size=None, translate=True) reference = fields.Char('Reference', size=None, translate=True, states=STATES, help='Use for purchase orders') party = fields.Many2One('party.party', 'Party', required=True, states=STATES, on_change=['party']) product = fields.Many2One('product.product', 'Product', required=True, states=STATES) list_price = fields.Numeric('List Price', states=STATES, digits=(16, 4), help='''Fixed-price override. Leave at 0.0000 for no override. Use discount at 100% to set actual price to 0.0''') discount = fields.Numeric('Discount (%)', states=STATES, digits=(4,2), help='Discount percentage on the list_price') quantity = fields.Numeric('Quantity', digits=(16,2), states=STATES) payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', required=True, states=STATES) state = fields.Selection([ ('draft','Draft'), ('active','Active'), ('hold', 'Hold'), ('canceled','Canceled'), ], 'State', readonly=True) interval = fields.Selection([ ('day','Day'), ('week','Week'), ('month','Month'), ('year','Year'), ], 'Interval', required=True, states=STATES) interval_quant = fields.Integer('Interval count', states=STATES) next_invoice_date = fields.Date('Next Invoice', states=STATES) opt_invoice_date = fields.Date('Temporary Next Invoice', readonly=True) start_date = fields.Date('Since', states=STATES) stop_date = fields.Date('Until') lines = fields.One2Many('account.invoice.line', 'contract', 'Invoice Lines', readonly=True, domain=[('contract','=',Eval('id'))]) def __init__(self): super(Contract, self).__init__() self._rpc.update({ 'create_next_invoice': True, 'create_invoice_batch': True, 'cancel_with_credit': True, }) def default_state(self): return 'draft' def default_interval(self): return 'month' def default_quantity(self): return Decimal('1.0') def default_company(self): return Transaction().context.get('company') or False def default_start_date(self): return datetime.date.fromtimestamp(time.time()) def default_interval_quant(self): return Decimal("1.0") def default_payment_term(self): config_obj = self.pool.get('contract.configuration') config = config_obj.browse(1) if config.payment_term: return config.payment_term.id company_id = self.default_company() company_obj = self.pool.get('company.company') company = company_obj.browse(company_id) if company and company.payment_term: return company.payment_term.id return False def default_journal(self): journal_obj = self.pool.get('account.journal') journal_ids = journal_obj.search([('type','=','revenue')], limit=1) if journal_ids: return journal_ids[0] return False def on_change_party(self, vals): party_obj = self.pool.get('party.party') address_obj = self.pool.get('party.address') payment_term_obj = self.pool.get('account.invoice.payment_term') res = { 'invoice_address': False, } if vals.get('party'): party = party_obj.browse(vals['party']) payment_term = party.payment_term or False discount = party.discount or Decimal("0.0") if payment_term: res['payment_term'] = payment_term.id res['payment_term.rec_name'] = payment_term_obj.browse( res['payment_term']).rec_name res['discount'] = discount return res def write(self, ids, vals): if 'state' in vals: self.workflow_trigger_trigger(ids) return super(Contract, self).write(ids, vals) def _invoice_init(self, contract, invoice_date): invoice_obj = self.pool.get('account.invoice') invoice_address = contract.party.address_get(contract.party.id, type='invoice') config_obj = self.pool.get('contract.configuration') config = config_obj.browse(1) description = config.description invoice = invoice_obj.create(dict( company=contract.company.id, type='out_invoice', description=description, state='draft', currency=contract.company.currency.id, journal=contract.journal.id, account=contract.party.account_receivable.id or contract.company.account_receivable.id, payment_term=contract.party.payment_term.id or contract.payment_term.id, party=contract.party.id, invoice_address=invoice_address, invoice_date=invoice_date, )) return invoice_obj.browse([invoice])[0] def _contract_unit_price(self, contract): unit_price = contract.product.list_price if contract.list_price: unit_price = contract.list_price elif contract.discount: unit_price = contract.product.list_price - (contract.product.list_price * contract.discount / 100) return unit_price def _invoice_append(self, invoice, contract, period): (last_date, next_date, quant) = period line_obj = self.pool.get('account.invoice.line') quantity = Decimal("%f" % quant) unit_price = self._contract_unit_price(contract) if not unit_price: # skip this contract log.info("skip contract without unit_price: %s contract.product.list_price: %s contract.list_price: %s contract.discount: %s" %( unit_price, contract.product.list_price, contract.list_price, contract.discount)) return linedata = dict( type='line', product=contract.product.id, invoice=invoice.id, description="%s: %s (%s - %s)" % (contract.name, contract.product.name, last_date, next_date), quantity=quantity, unit=contract.product.default_uom.id, unit_price=unit_price, contract=contract.id, taxes=[], ) account = contract.product.get_account([contract.product.id],'account_revenue_used') if account: linedata['account'] = account.popitem()[1] tax_rule_obj = self.pool.get('account.tax.rule') taxes = contract.product.get_taxes([contract.product.id], 'customer_taxes_used') for tax in taxes[contract.product.id]: pattern = {} if contract.party.customer_tax_rule: tax_ids = tax_rule_obj.apply(contract.party.customer_tax_rule, tax, pattern) if tax_ids: for tax_id in tax_ids: linedata['taxes'].append(('add',tax_id)) continue linedata['taxes'].append(('add',tax)) if contract.reference and not invoice.reference: invoice_obj = self.pool.get('account.invoice') invoice_obj.write(invoice.id, {'reference': contract.reference}) return line_obj.create(linedata) def cancel_with_credit(self, ids): """ Contract is canceled. Open invoices are credited. i.e. in case of failure to provide proper and valid credentials such as an initial payment. """ contract_obj = self.pool.get('contract.contract') contract = self.browse(ids)[0] if not contract.state == 'active': return {} for id in ids: self.workflow_trigger_validate(id, 'cancel') invoice_obj = self.pool.get('account.invoice') invoice_line_obj = self.pool.get('account.invoice.line') line_ids = invoice_line_obj.search([('contract','in',ids)]) if not line_ids: return {} invoices = [] lines = invoice_line_obj.browse(line_ids) for l in lines: invoices.append(l.invoice.id) invoice_obj.credit(invoices, refund=True) return {} def create_next_invoice(self, ids, data=None): if data.get('form') and data['form'].get('invoice_date'): invoice_date = data['form']['invoice_date'] else: invoice_date = datetime.date.today() contract_obj = self.pool.get('contract.contract') contract = self.browse(ids)[0] period = self._check_contract(contract, invoice_date) if not period: return {} log.debug("invoice_date: %s period: %s" %(invoice_date, period)) ## create a new invoice invoice = self._invoice_init(contract, invoice_date) ## create invoice lines line = self._invoice_append(invoice, contract, period) self.write(contract.id, {'opt_invoice_date': period[1]}) return invoice.id def _check_contract(self, contract, invoice_date): """ returns tuple 'period' or False False is returned if this contract is not up for billing at this moment. period is defined as (last_date, next_date, quantity) where the first two are datetime values indicating the billing period this contract is valid for, and the last indicates the quantity on the invoice line (months, weeks, years) """ if not contract.state == 'active': return {} # don't invoice contracts unless they are due within 30 days # after the invoice_date end = invoice_date + datetime.timedelta(NOTICE_DAYS) if contract.next_invoice_date and end < contract.next_invoice_date: log.info('too early to invoice: %s + %d days < %s' %(invoice_date, NOTICE_DAYS, end)) return False last_date = contract.next_invoice_date or contract.start_date or invoice_date next_date = last_date quant = 0 while next_date < end: if contract.interval == 'year': next_date = next_date + relativedelta(years=contract.interval_quant) quant = relativedelta(next_date, last_date).years elif contract.interval == 'month': next_date = next_date + relativedelta(months=contract.interval_quant) delta = relativedelta(next_date, last_date) quant = delta.years * 12 + delta.months elif contract.interval == 'week': next_date = next_date + relativedelta(weeks=contract.interval_quant) quant = (next_date - last_date).days/7 elif contract.interval == 'day': next_date = next_date + relativedelta(days=contract.interval_quant) quant = (next_date - last_date).days if next_date and contract.stop_date and next_date > contract.stop_date: log.info('contract stopped: %s > %s' % (next_date, contract.stop_date)) return False quant = quant * contract.quantity log.debug("last_date: %s next_date: %s quant: %d" % (last_date,next_date,quant)) return (last_date, next_date, quant) def create_invoice_batch(self, party=None, data=None): if data and data.get('form') and data['form'].get('invoice_date'): invoice_date = data['form']['invoice_date'] else: invoice_date = datetime.date.today() log.info("create invoice batch with invoice_date: %s" % invoice_date) contract_obj = self.pool.get('contract.contract') contract_ids = None if data and data.get('model') == 'contract.contract': contract_ids = data.get('ids') if not contract_ids: """ get a list of all active contracts """ query = [('state','=','active'), ('start_date','<=',invoice_date), ] """ filter on party if required """ if party: if type(party) != type([1,]): party = [party,] query.append(('party','in',party)) contract_ids = contract_obj.search(query) if not contract_ids: return [] """ build the list of all billable contracts and aggragate the result per party """ batch = {} contracts = contract_obj.browse(contract_ids) for contract in contracts: period = self._check_contract(contract, invoice_date) if period and period[2]: key = contract.party.id if not batch.get(key): batch[key] = [] batch[key].append((contract, period)) """ create draft invoices per party with lines for all billable contracts """ res = [] for party, info in batch.items(): invoice = self._invoice_init(info[0][0], invoice_date) for (contract, period) in info: self._invoice_append(invoice, contract, period) self.write(contract.id, {'opt_invoice_date': period[1]}) res.append(invoice.id) return res
class IncomeTaxRule(ModelSQL, ModelView): """ Income Tax Rules""" __name__ = 'income_tax.rule' name = fields.Char('Rule Name') start_date = fields.Date('Start Date') end_date = fields.Date('End Date') fiscal_year = fields.Many2One('account.fiscalyear', 'Fiscal Year') gender = fields.Selection([ ('not_applicable', 'Not Applicable'), ('male', 'Male'), ('female', 'Female'), ('other', "Other")], 'Gender') born_after = fields.Date('Born after') born_before = fields.Date('Born before') cess = fields.Integer('Cess %') rule_lines = fields.One2Many('income_tax.slab', 'income_tax_rule', 'Rules') @staticmethod def default_gender(): '''returns gender as Not applicable default value''' return 'not_applicable' @staticmethod def default_fiscal_year(): ''' returns start_date as this year's current fiscal year's start_date value. ''' pool = Pool() fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) return current_fiscal_year @classmethod def get_annual_income_tax(cls, employee, income): '''Calculate the income tax based on employee's record, current fiscal year and applicable tax slab ''' projected_income_tax = 0 taxable_income = income pool = Pool() fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) tax_rule = pool.get('income_tax.rule') tax_rules = tax_rule.search([ ('fiscal_year', '=', current_fiscal_year), ]) for rule in tax_rules: for slab in rule.rule_lines: if slab.from_amount <= taxable_income and \ slab.to_amount >= taxable_income: projected_income_tax = (slab.percentage/100)*taxable_income return projected_income_tax @classmethod def get_income_tax_ytd(cls, employee, income): ''' Calculate the income tax deducted based on employee's record, current fiscal year and applicable tax slab TODO: Review the docstring and method ''' taxable_income_ytd = income pool = Pool() fiscal = pool.get('account.fiscalyear') company = Transaction().context.get('company') current_fiscal_year = fiscal.find(company) tax_rule = pool.get('income_tax.rule') tax_rules = tax_rule.search([ ('fiscal_year', '=', current_fiscal_year), ]) for rule in tax_rules: for slab in rule.rule_lines: if slab.from_amount <= taxable_income_ytd and \ slab.to_amount >= taxable_income_ytd: annual_income_tax = (slab.percentage/100) \ * taxable_income_ytd return annual_income_tax
class Book(ModelSQL, ModelView): 'Book' __name__ = 'library.book' _rec_name = 'title' author = fields.Many2One('library.author', 'Author', required=True, ondelete='CASCADE') exemplaries = fields.One2Many('library.book.exemplary', 'book', 'Exemplaries') title = fields.Char('Title', required=True) genre = fields.Many2One('library.genre', 'Genre', ondelete='RESTRICT', domain=[('editors', '=', Eval('editor'))], depends=['editor'], required=False) editor = fields.Many2One( 'library.editor', 'Editor', ondelete='RESTRICT', domain=[ If(Bool(Eval('publishing_date', False)), [('creation_date', '<=', Eval('publishing_date'))], []) ], required=True, depends=['publishing_date']) isbn = fields.Char('ISBN', size=13, help='The International Standard Book Number') publishing_date = fields.Date('Publishing date') description = fields.Char('Description') summary = fields.Text('Summary') cover = fields.Binary('Cover') page_count = fields.Integer('Page Count', help='The number of page in the book') edition_stopped = fields.Boolean( 'Edition stopped', help='If True, this book will not be printed again in this version') number_of_exemplaries = fields.Function( fields.Integer('Number of exemplaries'), 'getter_number_of_exemplaries') latest_exemplary = fields.Function( fields.Many2One('library.book.exemplary', 'Latest exemplary'), 'getter_latest_exemplary') @classmethod def __setup__(cls): super().__setup__() t = cls.__table__() cls._sql_constraints += [ ('author_title_uniq', Unique(t, t.author, t.title), 'The title must be unique per author!'), ] cls._error_messages.update({ 'invalid_isbn': 'ISBN should only be digits', 'bad_isbn_size': 'ISBN must have 13 digits', 'invalid_isbn_checksum': 'ISBN checksum invalid', }) cls._buttons.update({ 'create_exemplaries': {}, }) @classmethod def validate(cls, books): for book in books: if not book.isbn: continue try: if int(book.isbn) < 0: raise ValueError except ValueError: cls.raise_user_error('invalid_isbn') if len(book.isbn) != 13: cls.raise_user_error('bad_isbn_size') checksum = 0 for idx, digit in enumerate(book.isbn): checksum += int(digit) * (1 if idx % 2 else 3) if checksum % 10: cls.raise_user_error('invalid_isbn_checksum') @classmethod def default_exemplaries(cls): return [] @fields.depends('editor', 'genre') def on_change_editor(self): if not self.editor: return if self.genre and self.genre not in self.editor.genres: self.genre = None if not self.genre and len(self.editor.genres) == 1: self.genre = self.editor.genres[0] @fields.depends('description', 'summary') def on_change_with_description(self): if self.description: return self.description if not self.summary: return '' return self.summary.split('.')[0] @fields.depends('exemplaries') def on_change_with_number_of_exemplaries(self): return len(self.exemplaries or []) def getter_latest_exemplary(self, name): latest = None for exemplary in self.exemplaries: if not exemplary.acquisition_date: continue if not latest or (latest.acquisition_date < exemplary.acquisition_date): latest = exemplary return latest.id if latest else None @classmethod def getter_number_of_exemplaries(cls, books, name): result = {x.id: 0 for x in books} Exemplary = Pool().get('library.book.exemplary') exemplary = Exemplary.__table__() cursor = Transaction().connection.cursor() cursor.execute( *exemplary.select(exemplary.book, Count(exemplary.id), where=exemplary.book.in_([x.id for x in books]), group_by=[exemplary.book])) for book_id, count in cursor.fetchall(): result[book_id] = count return result @classmethod @ModelView.button_action('library.act_create_exemplaries') def create_exemplaries(cls, books): pass
class Product(DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin): "Product Variant" __name__ = "product.product" _order_name = 'rec_name' template = fields.Many2One('product.template', 'Product Template', required=True, ondelete='CASCADE', select=True, states=STATES, depends=DEPENDS) code_readonly = fields.Function(fields.Boolean('Code Readonly'), 'get_code_readonly') code = fields.Char("Code", size=None, select=True, states={ 'readonly': STATES['readonly'] | Eval('code_readonly', False), }, depends=DEPENDS + ['code_readonly']) identifiers = fields.One2Many('product.identifier', 'product', "Identifiers", states=STATES, depends=DEPENDS, help="Add other identifiers to the variant.") cost_price = fields.MultiValue( fields.Numeric("Cost Price", required=True, digits=price_digits, states=STATES, depends=DEPENDS)) cost_prices = fields.One2Many('product.cost_price', 'product', "Cost Prices") description = fields.Text("Description", translate=True, states=STATES, depends=DEPENDS) list_price_uom = fields.Function( fields.Numeric('List Price', digits=price_digits), 'get_price_uom') cost_price_uom = fields.Function( fields.Numeric('Cost Price', digits=price_digits), 'get_price_uom') @classmethod def __setup__(cls): pool = Pool() Template = pool.get('product.template') if not hasattr(cls, '_no_template_field'): cls._no_template_field = set() cls._no_template_field.update(['products']) super(Product, cls).__setup__() for attr in dir(Template): tfield = getattr(Template, attr) if not isinstance(tfield, fields.Field): continue if attr in cls._no_template_field: continue field = getattr(cls, attr, None) if not field or isinstance(field, TemplateFunction): setattr(cls, attr, TemplateFunction(copy.deepcopy(tfield))) order_method = getattr(cls, 'order_%s' % attr, None) if (not order_method and not isinstance( tfield, (fields.Function, fields.One2Many, fields.Many2Many))): order_method = TemplateFunction.order(attr) setattr(cls, 'order_%s' % attr, order_method) if isinstance(tfield, fields.One2Many): getattr(cls, attr).setter = '_set_template_function' @classmethod def _set_template_function(cls, products, name, value): # Prevent NotImplementedError for One2Many pass @fields.depends('template', '_parent_template.id') def on_change_template(self): for name, field in self._fields.items(): if isinstance(field, TemplateFunction): if self.template: value = getattr(self.template, name, None) else: value = None setattr(self, name, value) def get_template(self, name): value = getattr(self.template, name) if isinstance(value, Model): return value.id elif (isinstance(value, (list, tuple)) and value and isinstance(value[0], Model)): return [r.id for r in value] else: return value @classmethod def multivalue_model(cls, field): pool = Pool() if field == 'cost_price': return pool.get('product.cost_price') return super(Product, cls).multivalue_model(field) @classmethod def default_cost_price(cls, **pattern): return Decimal(0) @classmethod def search_template(cls, name, clause): return [('template.' + clause[0], ) + tuple(clause[1:])] @classmethod def order_rec_name(cls, tables): pool = Pool() Template = pool.get('product.template') product, _ = tables[None] if 'template' not in tables: template = Template.__table__() tables['template'] = { None: (template, product.template == template.id), } else: template = tables['template'] return [product.code] + Template.name.convert_order( 'name', tables['template'], Template) def get_rec_name(self, name): if self.code: return '[' + self.code + '] ' + self.name else: return self.name @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' code_value = clause[2] if clause[1].endswith('like'): code_value = lstrip_wildcard(clause[2]) return [ bool_op, ('code', clause[1], code_value) + tuple(clause[3:]), ('identifiers.code', clause[1], code_value) + tuple(clause[3:]), ('template.name', ) + tuple(clause[1:]), ] @staticmethod def get_price_uom(products, name): Uom = Pool().get('product.uom') res = {} field = name[:-4] if Transaction().context.get('uom'): to_uom = Uom(Transaction().context['uom']) else: to_uom = None for product in products: price = getattr(product, field) if to_uom and product.default_uom.category == to_uom.category: res[product.id] = Uom.compute_price(product.default_uom, price, to_uom) else: res[product.id] = price return res @classmethod def search_global(cls, text): for id_, rec_name, icon in super(Product, cls).search_global(text): icon = icon or 'tryton-product' yield id_, rec_name, icon @classmethod def default_code_readonly(cls): pool = Pool() Configuration = pool.get('product.configuration') config = Configuration(1) return bool(config.product_sequence) def get_code_readonly(self, name): return self.default_code_readonly() @classmethod def _new_code(cls): pool = Pool() Sequence = pool.get('ir.sequence') Configuration = pool.get('product.configuration') config = Configuration(1) sequence = config.product_sequence if sequence: return Sequence.get_id(sequence.id) @classmethod def create(cls, vlist): vlist = [x.copy() for x in vlist] for values in vlist: if not values.get('code'): values['code'] = cls._new_code() return super().create(vlist) @classmethod def copy(cls, products, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('code', None) return super().copy(products, default=default) @property def list_price_used(self): return self.template.get_multivalue('list_price')
class PaymentTerm(ModelSQL, ModelView): 'Payment Term' __name__ = 'account.invoice.payment_term' name = fields.Char('Name', size=None, required=True, translate=True) active = fields.Boolean('Active') description = fields.Text('Description', translate=True) lines = fields.One2Many('account.invoice.payment_term.line', 'payment', 'Lines') @classmethod def __setup__(cls): super(PaymentTerm, cls).__setup__() cls._order.insert(0, ('name', 'ASC')) cls._error_messages.update({ 'invalid_line': ('Invalid line "%(line)s" in payment term ' '"%(term)s".'), 'missing_remainder': ('Missing remainder line in payment term ' '"%s".'), 'last_remainder': ('Last line of payment term "%s" must be of ' 'type remainder.'), }) @classmethod def validate(cls, terms): super(PaymentTerm, cls).validate(terms) for term in terms: term.check_remainder() def check_remainder(self): if not self.lines or not self.lines[-1].type == 'remainder': self.raise_user_error('last_remainder', self.rec_name) @staticmethod def default_active(): return True def compute(self, amount, currency, date=None): """Calculate payment terms and return a list of tuples with (date, amount) for each payment term line. amount must be a Decimal used for the calculation. If specified, date will be used as the start date, otherwise current date will be used. """ # TODO implement business_days # http://pypi.python.org/pypi/BusinessHours/ Date = Pool().get('ir.date') sign = 1 if amount >= Decimal('0.0') else -1 res = [] if date is None: date = Date.today() remainder = amount for line in self.lines: value = line.get_value(remainder, amount, currency) value_date = line.get_date(date) if not value or not value_date: if (not remainder) and line.amount: self.raise_user_error('invalid_line', { 'line': line.rec_name, 'term': self.rec_name, }) else: continue if ((remainder - value) * sign) < Decimal('0.0'): res.append((value_date, remainder)) break res.append((value_date, value)) remainder -= value if not currency.is_zero(remainder): self.raise_user_error('missing_remainder', (self.rec_name, )) return res
class Template(DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin): "Product Template" __name__ = "product.template" name = fields.Char('Name', size=None, required=True, translate=True, select=True, states=STATES, depends=DEPENDS) type = fields.Selection(TYPES, 'Type', required=True, states=STATES, depends=DEPENDS) consumable = fields.Boolean('Consumable', states={ 'readonly': ~Eval('active', True), 'invisible': Eval('type', 'goods') != 'goods', }, depends=['active', 'type']) list_price = fields.MultiValue( fields.Numeric("List Price", required=True, digits=price_digits, states=STATES, depends=DEPENDS)) list_prices = fields.One2Many('product.list_price', 'template', "List Prices") cost_price = fields.Function( fields.Numeric("Cost Price", digits=price_digits), 'get_cost_price') cost_price_method = fields.MultiValue( fields.Selection(COST_PRICE_METHODS, "Cost Price Method", required=True, states=STATES, depends=DEPENDS)) cost_price_methods = fields.One2Many('product.cost_price_method', 'template', "Cost Price Methods") default_uom = fields.Many2One('product.uom', 'Default UOM', required=True, states=STATES, depends=DEPENDS) default_uom_category = fields.Function( fields.Many2One('product.uom.category', 'Default UOM Category'), 'on_change_with_default_uom_category', searcher='search_default_uom_category') categories = fields.Many2Many('product.template-product.category', 'template', 'category', 'Categories', states=STATES, depends=DEPENDS) categories_all = fields.Many2Many('product.template-product.category.all', 'template', 'category', "Categories", readonly=True) products = fields.One2Many('product.product', 'template', 'Variants', states=STATES, depends=DEPENDS) @classmethod def __register__(cls, module_name): super(Template, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.8: rename category into categories if table.column_exist('category'): logger.warning( 'The column "category" on table "%s" must be dropped manually', cls._table) @classmethod def multivalue_model(cls, field): pool = Pool() if field == 'list_price': return pool.get('product.list_price') elif field == 'cost_price_method': return pool.get('product.cost_price_method') return super(Template, cls).multivalue_model(field) @staticmethod def default_type(): return 'goods' @staticmethod def default_consumable(): return False def get_cost_price(self, name): if len(self.products) == 1: product, = self.products return product.cost_price @classmethod def default_cost_price_method(cls, **pattern): pool = Pool() Configuration = pool.get('product.configuration') return Configuration(1).get_multivalue('default_cost_price_method', **pattern) @staticmethod def default_products(): if Transaction().user == 0: return [] return [{}] @fields.depends('type', 'cost_price_method') def on_change_type(self): if self.type == 'service': self.cost_price_method = 'fixed' @fields.depends('default_uom') def on_change_with_default_uom_category(self, name=None): if self.default_uom: return self.default_uom.category.id @classmethod def search_default_uom_category(cls, name, clause): return [('default_uom.category' + clause[0].lstrip(name), ) + tuple(clause[1:])] @classmethod def create(cls, vlist): vlist = [v.copy() for v in vlist] for values in vlist: values.setdefault('products', None) return super(Template, cls).create(vlist) @classmethod def search_global(cls, text): for record, rec_name, icon in super(Template, cls).search_global(text): icon = icon or 'tryton-product' yield record, rec_name, icon
class CrmSegmentation(ModelSQL, ModelView): ''' A segmentation is a tool to automatically assign categories on partys. These assignations are based on criterions. ''' _name = "ekd.crm.segmentation" _description = "Party Segmentation" name = fields.Char('Name', size=64, required=True, help='The name of the segmentation.') description = fields.Text('Description') categ = fields.Many2One('party.category', 'party Category', required=True, help='The party category that will be added to partys that match the segmentation criterions after computation.') exclusif = fields.Boolean('Exclusive', help='Check if the category is limited to partys that match the segmentation criterions. If checked, remove the category from partys that doesn\'t match segmentation criterions') state = fields.Selection([('not running','Not Running'),('running','Running')], 'Execution Status', readonly=True) party = fields.Integer('Max party ID processed') segmentation_line = fields.One2Many('ekd.crm.segmentation.line', 'segmentation', 'Criteria', required=True) som_interval = fields.Integer('Days per Periode', help="A period is the average number of days between two cycle of sale or purchase for this segmentation. It's mainly used to detect if a party has not purchased or buy for a too long time, so we suppose that his state of mind has decreased because he probably bought goods to another supplier. Use this functionality for recurring businesses.") som_interval_max = fields.Integer('Max Interval', help="The computation is made on all events that occured during this interval, the past X periods.") som_interval_decrease = fields.Float('Decrease (0>1)', help="If the party has not purchased (or bought) during a period, decrease the state of mind by this factor. It\'s a multiplication") som_interval_default = fields.Float('Default (0=None)', help="Default state of mind for period preceeding the 'Max Interval' computation. This is the starting state of mind by default if the party has no event.") sales_purchase_active = fields.Boolean('Use The Sales Purchase Rules', help='Check if you want to use this tab as part of the segmentation rule. If not checked, the criteria beneath will be ignored') def __init__(self): super(CrmSegmentation, self).__init__() self._rpc.update({ 'button_process_start': True, 'button_process_stop': True, 'button_process_continue': True, }) def default_party(self): return 0 def default_state(self): return 'not running' def default_som_interval_max(self): return 3 def default_som_interval_decrease(self): return Decimal('0.8') def default_som_interval_default(self): return Decimal('0.5') def button_process_continue(self, ids, start=False): cr = Transaction().cursor categs = self.read(ids,['category','exclusif','party', 'sales_purchase_active', 'profiling_active']) for categ in categs: if start: if categ['exclusif']: cr.execute('delete from party_category_rel where category=%s', (categ['categ'][0],)) id = categ['id'] cr.execute('select id from res_party order by id ') partys = [x[0] for x in cr.fetchall()] if categ['sales_purchase_active']: to_remove_list=[] cr.execute('select id from ekd_crm_segmentation_line where segmentation=%s', (id,)) line_ids = [x[0] for x in cr.fetchall()] for pid in partys: if (not self.pool.get('ekd.crm.segmentation.line').test(cr, uid, line_ids, pid)): to_remove_list.append(pid) for pid in to_remove_list: partys.remove(pid) for party in partys: cr.execute('insert into party_category_rel (category,party) values (%s,%s)', (categ['categ'][0],party)) cr.commit() self.write([id], {'state':'not running', 'party':0}) cr.commit() return True def button_process_stop(self, ids, *args): return self.write(ids, {'state':'not running', 'party':0}) def button_process_start(self, ids, *args): self.write(ids, {'state':'running', 'party':0}) return self.process_continue(cr, uid, ids, start=True)
class Procedure(ModelSQL, ModelView): 'Account Dunning Procedure' __name__ = 'account.dunning.procedure' name = fields.Char('Name', required=True, translate=True) levels = fields.One2Many('account.dunning.level', 'procedure', 'Levels')
class Party(CompanyMultiValueMixin, metaclass=PoolMeta): __name__ = 'party.party' accounts = fields.One2Many('party.party.account', 'party', "Accounts") account_payable = fields.MultiValue( fields.Many2One('account.account', "Account Payable", domain=[ ('kind', '=', 'payable'), ('party_required', '=', True), ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'invisible': ~Eval('context', {}).get('company'), })) account_receivable = fields.MultiValue( fields.Many2One('account.account', "Account Receivable", domain=[ ('kind', '=', 'receivable'), ('party_required', '=', True), ('company', '=', Eval('context', {}).get('company', -1)), ], states={ 'invisible': ~Eval('context', {}).get('company'), })) customer_tax_rule = fields.MultiValue( fields.Many2One( 'account.tax.rule', "Customer Tax Rule", domain=[ ('company', '=', Eval('context', {}).get('company', -1)), ('kind', 'in', ['sale', 'both']), ], states={ 'invisible': ~Eval('context', {}).get('company'), }, help='Apply this rule on taxes when party is customer.')) supplier_tax_rule = fields.MultiValue( fields.Many2One( 'account.tax.rule', "Supplier Tax Rule", domain=[ ('company', '=', Eval('context', {}).get('company', -1)), ('kind', 'in', ['purchase', 'both']), ], states={ 'invisible': ~Eval('context', {}).get('company'), }, help='Apply this rule on taxes when party is supplier.')) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'get_currency_digits') receivable = fields.Function(fields.Numeric( 'Receivable', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_receivable_payable', searcher='search_receivable_payable') payable = fields.Function(fields.Numeric( 'Payable', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_receivable_payable', searcher='search_receivable_payable') receivable_today = fields.Function(fields.Numeric( 'Receivable Today', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_receivable_payable', searcher='search_receivable_payable') payable_today = fields.Function(fields.Numeric( 'Payable Today', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_receivable_payable', searcher='search_receivable_payable') @classmethod def __setup__(cls): super(Party, cls).__setup__() cls._error_messages.update({ 'missing_receivable_account': ('There is no receivable account on party "%(name)s".'), 'missing_payable_account': ('There is no payable account on party "%(name)s".'), }) @classmethod def multivalue_model(cls, field): pool = Pool() if field in account_names: return pool.get('party.party.account') return super(Party, cls).multivalue_model(field) @classmethod def get_currency_digits(cls, parties, name): pool = Pool() Company = pool.get('company.company') company_id = Transaction().context.get('company') if company_id: company = Company(company_id) digits = company.currency.digits else: digits = 2 return {p.id: digits for p in parties} @classmethod def get_receivable_payable(cls, parties, names): ''' Function to compute receivable, payable (today or not) for party ids. ''' result = {} pool = Pool() MoveLine = pool.get('account.move.line') Account = pool.get('account.account') User = pool.get('res.user') Date = pool.get('ir.date') cursor = Transaction().connection.cursor() line = MoveLine.__table__() account = Account.__table__() for name in names: if name not in ('receivable', 'payable', 'receivable_today', 'payable_today'): raise Exception('Bad argument') result[name] = dict((p.id, Decimal('0.0')) for p in parties) user = User(Transaction().user) if not user.company: return result company_id = user.company.id exp = Decimal(str(10.0**-user.company.currency.digits)) amount = Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0)) for name in names: code = name today_where = Literal(True) if name in ('receivable_today', 'payable_today'): code = name[:-6] today_where = ((line.maturity_date <= Date.today()) | (line.maturity_date == Null)) for sub_parties in grouped_slice(parties): sub_ids = [p.id for p in sub_parties] party_where = reduce_ids(line.party, sub_ids) cursor.execute( *line.join(account, condition=account.id == line.account). select(line.party, amount, where=((account.kind == code) & (line.reconciliation == Null) & (account.company == company_id) & party_where & today_where), group_by=line.party)) for party, value in cursor.fetchall(): # SQLite uses float for SUM if not isinstance(value, Decimal): value = Decimal(str(value)) result[name][party] = value.quantize(exp) return result @classmethod def search_receivable_payable(cls, name, clause): pool = Pool() MoveLine = pool.get('account.move.line') Account = pool.get('account.account') User = pool.get('res.user') Date = pool.get('ir.date') line = MoveLine.__table__() account = Account.__table__() if name not in ('receivable', 'payable', 'receivable_today', 'payable_today'): raise Exception('Bad argument') _, operator, value = clause user = User(Transaction().user) if not user.company: return [] company_id = user.company.id code = name today_query = Literal(True) if name in ('receivable_today', 'payable_today'): code = name[:-6] today_query = ((line.maturity_date <= Date.today()) | (line.maturity_date == Null)) Operator = fields.SQL_OPERATORS[operator] # Need to cast numeric for sqlite cast_ = MoveLine.debit.sql_cast amount = cast_(Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0))) if operator in {'in', 'not in'}: value = [cast_(Literal(Decimal(v or 0))) for v in value] else: value = cast_(Literal(Decimal(value or 0))) query = line.join(account, condition=account.id == line.account).select( line.party, where=(account.kind == code) & (line.party != Null) & (line.reconciliation == Null) & (account.company == company_id) & today_query, group_by=line.party, having=Operator(amount, value)) return [('id', 'in', query)] @property def account_payable_used(self): pool = Pool() Configuration = pool.get('account.configuration') account = self.account_payable if not account: config = Configuration(1) account = config.get_multivalue('default_account_payable') # Allow empty values on on_change if not account and not Transaction().readonly: self.raise_user_error('missing_payable_account', { 'name': self.rec_name, }) if account: return account.current() @property def account_receivable_used(self): pool = Pool() Configuration = pool.get('account.configuration') account = self.account_receivable if not account: config = Configuration(1) account = config.get_multivalue('default_account_receivable') # Allow empty values on on_change if not account and not Transaction().readonly: self.raise_user_error('missing_receivable_account', { 'name': self.rec_name, }) if account: return account.current()
class CreateShipmentInReturn(ModelView): 'CreateShipmentInReturn' __name__ = 'hrp_purchase_request.CreateShipmentInReturn' _rec_name = 'number' effective_date = fields.Date('Effective Date', states={ 'readonly': Eval('state').in_(['cancel', 'done']), }, depends=['state']) planned_date = fields.Date('Planned Date', states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) company = fields.Many2One( 'company.company', 'Company', required=True, states={ 'readonly': Eval('state') != 'draft', }, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], depends=['state']) number = fields.Char('Number', size=None, select=True, readonly=True) reference = fields.Char("Reference", size=None, select=True, states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) supplier = fields.Many2One('party.party', 'Supplier', states={ 'readonly': (((Eval('state') != 'draft') | Eval('moves', [0])) & Eval('supplier', 0)), }, required=True, depends=['state', 'supplier']) delivery_address = fields.Many2One('party.address', 'Delivery Address', states={ 'readonly': Eval('state') != 'draft', }, domain=[('party', '=', Eval('supplier')) ], depends=['state', 'supplier']) from_location = fields.Many2One( 'stock.location', "From Location", required=True, states={ 'readonly': (Eval('state') != 'draft') | Eval('moves', [0]), }, domain=[('type', 'in', ['storage', 'view'])], depends=['state']) to_location = fields.Many2One('stock.location', "To Location", required=True, states={ 'readonly': (Eval('state') != 'draft') | Eval('moves', [0]), }, domain=[('type', '=', 'supplier')], depends=['state']) moves = fields.One2Many( 'stock.move', 'shipment', 'Moves', states={ 'readonly': (((Eval('state') != 'draft') | ~Eval('from_location')) & Eval('to_location')), }, domain=[ ('from_location', '=', Eval('from_location')), ('to_location', '=', Eval('to_location')), ('company', '=', Eval('company')), ], depends=['state', 'from_location', 'to_location', 'company']) origins = fields.Function(fields.Char('Origins'), 'get_origins') state = fields.Selection([ ('draft', 'Draft'), ('cancel', 'Canceled'), ('assigned', 'Assigned'), ('waiting', 'Waiting'), ('done', 'Done'), ], 'State', readonly=True) @staticmethod def default_planned_date(): return Pool().get('ir.date').today() @staticmethod def default_state(): return 'draft' @classmethod def default_warehouse(cls): Location = Pool().get('stock.location') locations = Location.search(cls.warehouse.domain) if len(locations) == 1: return locations[0].id @staticmethod def default_company(): return Transaction().context.get('company') @fields.depends('supplier') def on_change_supplier(self): self.contact_address = None if self.supplier: self.contact_address = self.supplier.address_get() @fields.depends('supplier') def on_change_with_supplier_location(self, name=None): if self.supplier: return self.supplier.supplier_location.id @classmethod def default_warehouse_input(cls): warehouse = cls.default_warehouse() if warehouse: return cls(warehouse=warehouse).on_change_with_warehouse_input() @fields.depends('warehouse') def on_change_with_warehouse_input(self, name=None): if self.warehouse: return self.warehouse.input_location.id @classmethod def default_warehouse_storage(cls): warehouse = cls.default_warehouse() if warehouse: return cls(warehouse=warehouse).on_change_with_warehouse_storage() @fields.depends('warehouse') def on_change_with_warehouse_storage(self, name=None): if self.warehouse: return self.warehouse.storage_location.id def get_incoming_moves(self, name): moves = [] for move in self.moves: if move.to_location.id == self.warehouse.input_location.id: moves.append(move.id) return moves @classmethod def set_incoming_moves(cls, shipments, name, value): if not value: return cls.write(shipments, { 'moves': value, }) def get_inventory_moves(self, name): moves = [] for move in self.moves: if (move.from_location.id == self.warehouse.input_location.id): moves.append(move.id) return moves @classmethod def set_inventory_moves(cls, shipments, name, value): if not value: return cls.write(shipments, { 'moves': value, }) @property def _move_planned_date(self): ''' Return the planned date for incoming moves and inventory_moves ''' return self.planned_date, self.planned_date def get_origins(self, name): return ', '.join( set(itertools.ifilter(None, (m.origin_name for m in self.moves)))) @classmethod def _get_inventory_moves(cls, incoming_move): pool = Pool() Move = pool.get('stock.move') if incoming_move.quantity <= 0.0: return None move = Move() move.product = incoming_move.product move.uom = incoming_move.uom move.quantity = incoming_move.quantity move.from_location = incoming_move.to_location move.to_location = incoming_move.shipment.warehouse.storage_location move.state = Move.default_state() # Product will be considered in stock only when the inventory # move will be made: move.planned_date = None move.company = incoming_move.company return move @classmethod def create_inventory_moves(cls, shipments): for shipment in shipments: # Use moves instead of inventory_moves because save reset before # adding new records and as set_inventory_moves is just a proxy to # moves, it will reset also the incoming_moves moves = list(shipment.moves) for incoming_move in shipment.incoming_moves: move = cls._get_inventory_moves(incoming_move) if move: moves.append(move) shipment.moves = moves shipment.save()
class AccountVoucher(metaclass=PoolMeta): __name__ = 'account.voucher' retenciones_efectuadas = fields.One2Many( 'account.retencion.efectuada', 'voucher', 'Retenciones Efectuadas', states={ 'invisible': Eval('voucher_type') != 'payment', 'readonly': Or(Eval('state') == 'posted', Eval('currency_code') != 'ARS'), }, depends=['voucher_type', 'state', 'currency_code']) retenciones_soportadas = fields.One2Many( 'account.retencion.soportada', 'voucher', 'Retenciones Soportadas', states={ 'invisible': Eval('voucher_type') != 'receipt', 'readonly': Or(Eval('state') == 'posted', Eval('currency_code') != 'ARS'), }, depends=['voucher_type', 'state', 'currency_code']) @fields.depends('retenciones_efectuadas', 'retenciones_soportadas') def on_change_with_amount(self, name=None): amount = super().on_change_with_amount(name) if self.retenciones_efectuadas: for retencion in self.retenciones_efectuadas: if retencion.amount: amount += retencion.amount if self.retenciones_soportadas: for retencion in self.retenciones_soportadas: if retencion.amount: amount += retencion.amount return amount def prepare_move_lines(self): move_lines = super().prepare_move_lines() Period = Pool().get('account.period') if self.voucher_type == 'receipt': if self.retenciones_soportadas: for retencion in self.retenciones_soportadas: move_lines.append({ 'debit': retencion.amount, 'credit': Decimal('0.0'), 'account': (retencion.tax.account.id if retencion.tax else None), 'move': self.move.id, 'journal': self.journal.id, 'period': Period.find(self.company.id, date=self.date), }) if self.voucher_type == 'payment': if self.retenciones_efectuadas: for retencion in self.retenciones_efectuadas: move_lines.append({ 'debit': Decimal('0.0'), 'credit': retencion.amount, 'account': (retencion.tax.account.id if retencion.tax else None), 'move': self.move.id, 'journal': self.journal.id, 'period': Period.find(self.company.id, date=self.date), }) return move_lines @classmethod @ModelView.button def post(cls, vouchers): pool = Pool() RetencionSoportada = pool.get('account.retencion.soportada') RetencionEfectuada = pool.get('account.retencion.efectuada') super().post(vouchers) for voucher in vouchers: if voucher.retenciones_soportadas: RetencionSoportada.write(list(voucher.retenciones_soportadas), { 'party': voucher.party.id, 'state': 'held', }) if voucher.retenciones_efectuadas: for retencion in voucher.retenciones_efectuadas: if not retencion.tax.sequence: raise UserError( gettext( 'account_retencion_ar.msg_missing_retencion_seq' )) RetencionEfectuada.write( [retencion], { 'party': voucher.party.id, 'name': retencion.tax.sequence.get(), 'state': 'issued', }) @classmethod @ModelView.button def cancel(cls, vouchers): pool = Pool() RetencionSoportada = pool.get('account.retencion.soportada') RetencionEfectuada = pool.get('account.retencion.efectuada') super().cancel(vouchers) for voucher in vouchers: if voucher.retenciones_soportadas: RetencionSoportada.write(list(voucher.retenciones_soportadas), { 'party': None, 'state': 'cancelled', }) if voucher.retenciones_efectuadas: RetencionEfectuada.write(list(voucher.retenciones_efectuadas), { 'party': None, 'state': 'cancelled', })
class TriageEntry(ModelSQL, ModelView): 'Triage Entry' __name__ = 'gnuhealth.triage.entry' firstname = fields.Char('First Name', states=REQD_IF_NOPATIENT) lastname = fields.Char('Last Name', states=REQD_IF_NOPATIENT) sex = fields.Selection([(None, '')] + SEX_OPTIONS, 'Sex', states=REQD_IF_NOPATIENT) age = fields.Char('Age', states=REQD_IF_NOPATIENT) sex_display = fields.Function(fields.Selection(SEX_OPTIONS, 'Sex'), 'get_sex_age_display') age_display = fields.Function(fields.Char('Age'), 'get_sex_age_display') id_type = fields.Selection(ID_TYPES, 'ID Type', states={ 'required': Bool(Eval('id_number')), 'readonly': Bool(Eval('patient')) }, sort=False) id_number = fields.Char( 'ID Number', states={'readonly': Or(Bool(Eval('patient')), Eval('done', False))}) id_display = fields.Function(fields.Char('UPI/MRN'), 'get_id_display', searcher='search_id') patient = fields.Many2One('gnuhealth.patient', 'Patient', states={ 'readonly': Or(~Eval('can_do_details', False), Eval('done', False)) }) priority = fields.Selection(TRIAGE_PRIO, 'ESI Priority', sort=False, help='Emergency Severity Index Triage Level', states={ 'invisible': ~(Eval('id', 0) > 0), 'readonly': Or(~Eval('can_do_details', False), Eval('done', False)) }) medical_alert = fields.Function(fields.Boolean( 'Medical Alert', states={ 'invisible': Or(Eval('can_do_details', False), ~In(Eval('status'), ['triage', 'pending']), ~In(Eval('priority'), ['99', '77'])) }), 'get_medical_alert', setter='set_medical_alert') injury = fields.Boolean('Injury', states=SIGNED_STATES) review = fields.Boolean('Review', states=SIGNED_STATES) status = fields.Selection(TRIAGE_STATUS, 'Status', sort=False, states={ 'readonly': Or(~Eval('can_do_details', False), Eval('done', False)) }) status_display = fields.Function(fields.Char('Status'), 'get_status_display') complaint = fields.Char('Primary Complaint', states=SIGNED_STATES) notes = fields.Text('Notes (edit)', states=SIGNED_STATES) note_entries = fields.One2Many('gnuhealth.triage.note', 'triage_entry', 'Note entries') note_display = fields.Function(fields.Text('Notes'), 'get_note_display') upi = fields.Function(fields.Char('UPI'), 'get_patient_party_field') name = fields.Function(fields.Char('Name'), 'get_name', searcher='search_name') patient_search = fields.Function( fields.One2Many('gnuhealth.patient', None, 'Patients'), 'patient_search_result') queue_entry = fields.One2Many('gnuhealth.patient.queue_entry', 'triage_entry', 'Queue Entry', size=1) encounter = fields.Many2One('gnuhealth.encounter', 'Encounter') # Vital Signs systolic = fields.Integer('Systolic Pressure', states=SIGNED_STATES) diastolic = fields.Integer('Diastolic Pressure', states=SIGNED_STATES) bpm = fields.Integer('Heart Rate (bpm)', help='Heart rate expressed in beats per minute', states=SIGNED_STATES) respiratory_rate = fields.Integer( 'Respiratory Rate', help='Respiratory rate expressed in breaths per minute', states=SIGNED_STATES) osat = fields.Integer('Oxygen Saturation', help='Oxygen Saturation(arterial).', states=SIGNED_STATES) temperature = fields.Float(u'Temperature (°C)', digits=(4, 1), help='Temperature in degrees celsius', states=SIGNED_STATES) # domain=[('temperature', '>', 25), ('temperature', '<', 50)]) childbearing_age = fields.Function(fields.Boolean('Childbearing Age'), 'get_childbearing_age') pregnant = fields.Boolean('Pregnant', states=STATE_NO_MENSES) lmp = fields.Date('Last Menstrual Period', states=STATE_NO_MENSES, help='Date last menstrual period started') glucose = fields.Float( 'Glucose (mmol/l)', digits=(5, 1), help='mmol/l. Reading from glucose meter', states=SIGNED_STATES, domain=[ 'OR', ('glucose', '=', None), ['AND', ('glucose', '>', 0), ('glucose', '<', 55.1)] ]) height = fields.Numeric('Height (cm)', digits=(4, 1), states=SIGNED_STATES) weight = fields.Numeric('Weight (kg)', digits=(3, 2), states=SIGNED_STATES) uri_ph = fields.Numeric('pH', digits=(1, 1), states=SIGNED_STATES) uri_specific_gravity = fields.Numeric('Specific Gravity', digits=(1, 3), states=SIGNED_STATES) uri_protein = fields.Selection('uri_selection', 'Protein', sort=False, states=SIGNED_STATES) uri_blood = fields.Selection('uri_selection', 'Blood', sort=False, states=SIGNED_STATES) uri_glucose = fields.Selection('uri_selection', 'Glucose', sort=False, states=SIGNED_STATES) uri_nitrite = fields.Selection('uri_nitrite_selection', 'Nitrite', sort=False, states=SIGNED_STATES) uri_bilirubin = fields.Selection('uri_selection', 'Bilirubin', sort=False, states=SIGNED_STATES) uri_leuko = fields.Selection('uri_selection', 'Leukocytes', sort=False, states=SIGNED_STATES) uri_ketone = fields.Selection('uri_selection', 'Ketone', sort=False, states=SIGNED_STATES) uri_urobili = fields.Selection('uri_selection', 'Urobilinogen', sort=False, states=SIGNED_STATES) malnutrition = fields.Boolean( 'Malnourished', help='Check this box if the patient show signs of malnutrition.', states=SIGNED_STATES) dehydration = fields.Selection( [(None, 'No'), ('mild', 'Mild'), ('moderate', 'Moderate'), ('severe', 'Severe')], 'Dehydration', sort=False, help='If the patient show signs of dehydration.', states=SIGNED_STATES) symp_fever = fields.Boolean('Fever', states=SIGNED_STATES) symp_respiratory = fields.Boolean('Respiratory', help="breathing problems", states=SIGNED_STATES) symp_jaundice = fields.Boolean('Jaundice', states=SIGNED_STATES) symp_rash = fields.Boolean('Rash', states=SIGNED_STATES) symp_hemorrhagic = fields.Boolean("Hemorrhagic", states=SIGNED_STATES) symp_neurological = fields.Boolean("Neurological", states=SIGNED_STATES) symp_arthritis = fields.Boolean("Arthralgia/Arthritis", states=SIGNED_STATES) symp_vomitting = fields.Boolean("Vomitting", states=SIGNED_STATES) symp_diarrhoea = fields.Boolean("Diarrhoea", states=SIGNED_STATES) recent_travel_contact = fields.Char( "Countries visited/Contact with traveller", states=SIGNED_STATES, help="Countries visited or from which there was contact with a " "traveller within the last six weeks") institution = fields.Many2One('gnuhealth.institution', 'Institution', states={'readonly': True}) _history = True # enable revision control from core can_do_details = fields.Function(fields.Boolean('Can do triage details'), 'get_do_details_perm') first_contact_time = fields.Function(fields.Text('First Contact Time'), 'get_first_time_contact') done = fields.Boolean('Done', states={'invisible': True}) end_time = fields.DateTime('End Time', help='Date and time triage ended', states={ 'readonly': Or(~Eval('can_do_details', False), Eval('done', False)) }) post_appointment = fields.Many2One('gnuhealth.appointment', 'Appointment') # signed_by = fields.Many2One('gnuhealth.healthprofessional'', 'Signed By') # sign_time = fields.DateTime('Signed on') total_time = fields.Function( fields.Char('Triage Time', states={'invisible': ~Eval('done', False)}), 'get_triage_time') @classmethod def __setup__(cls): super(TriageEntry, cls).__setup__() cls._buttons.update({ 'set_done': { 'readonly': ~Eval('can_do_details', False), 'invisible': Or(In(Eval('status'), ['pending', 'triage']), Eval('done', False)) }, 'go_referral': { 'readonly': ~Eval('can_do_details', False), 'invisible': ~In(Eval('status'), ['refer', 'referin']) } }) @classmethod def _swapnote(cls, vdict): '''swaps out the value in the notes field for an entry that creates a new gnuhealth.triage.note model instance''' new_note = vdict.get('notes', '') if new_note.strip(): new_note = new_note.strip() noteobj = ('create', [{'note': new_note}]) vdict.setdefault('note_entries', []).append(noteobj) vdict[ 'notes'] = u'' # toDo: remove this for next release and use vdict.pop return vdict @classmethod def make_priority_updates(cls, triage_entries, values_to_write): if ('priority' in values_to_write and 'queue_entry' not in values_to_write): prio = int(values_to_write['priority']) queue_model = Pool().get('gnuhealth.patient.queue_entry') qentries = queue_model.search([('triage_entry', 'in', triage_entries)]) values_to_write['queue_entry'] = [('write', map(int, qentries), { 'priority': prio })] # force end-time to now if none entered and the prompt ignored if (values_to_write.get('done', False) and not values_to_write.get('end_time', False)): values_to_write['end_time'] = datetime.now() return triage_entries, cls._swapnote(values_to_write) @classmethod def create(cls, vlist): # add me to the queue when created for vdict in vlist: if not vdict.get('queue_entry'): if vdict.get('medical_alert') is True: vqprio = MED_ALERT else: try: vqprio = int(vdict.get('priority', TRIAGE_MAX_PRIO)) except TypeError: vqprio = int(TRIAGE_MAX_PRIO) vdict['queue_entry'] = [('create', [{ 'busy': False, 'priority': vqprio }])] vdict = cls._swapnote(vdict) # in case there's a note now return super(TriageEntry, cls).create(vlist) @classmethod def write(cls, records, values, *args): # update queue priority when mine updated # but only if it's higher or there's no appointment records, values = cls.make_priority_updates(records, values) newargs = [] if args: arglist = iter(args) for r, v in zip(arglist, arglist): r, v = cls.make_priority_updates(r, v) newargs.extend([r, v]) return super(TriageEntry, cls).write(records, values, *newargs) @staticmethod def default_priority(): return str(TRIAGE_MAX_PRIO) @staticmethod def default_status(): return 'pending' def get_triage_time(self, name): endtime = self.end_time if self.done else datetime.now() return get_elapsed_time(self.create_date, endtime) def get_name(self, name): if name == 'name': if self.patient: return self.patient.name.name else: return '%s, %s' % (self.lastname, self.firstname) return '' @classmethod def search_name(cls, name, clause): fld, operator, operand = clause return [ 'OR', ('patient.name.name', operator, operand), ('firstname', operator, operand), ('lastname', operator, operand) ] @classmethod def search_id(cls, name, clause): fld, operator, operand = clause return [ 'OR' ('patient.name.upi', operator, operand), ('patient.medical_record_num', operator, operand), ('id_number', operator, operand) ] @classmethod def get_patient_party_field(cls, instances, name): out = dict([(i.id, '') for i in instances]) if name == 'upi': out.update([(i.id, i.patient.puid) for i in instances if i.patient]) return out def get_id_display(self, name): idtypedict = dict(ID_TYPES) if self.patient: return '{} / {}'.format(self.patient.puid, self.patient.medical_record_num) elif self.id_number and self.id_type: return ': '.join( [idtypedict.get(self.id_type, '??'), self.id_number]) else: return '' def patient_search_result(self, name): # ToDo: perform search against patient/party and return # the ones that match. # the domain should include : # lastname, firstname, sex, id_type, id_number return [] def get_status_display(self, name): return TRIAGE_STATUS_LOOKUP.get(self.status) def get_childbearing_age(self, name): if self.patient: return self.patient.childbearing_age elif self.sex == 'm': return False else: age = self.age if age.isdigit(): age = int(age) elif age[:-1].isdigit(): age = int(age[:-1]) else: age = 7 # hack to make a default false for BHC if age < MENARCH[0] or age > MENARCH[1]: return False return True def get_sex_age_display(self, name): field = name[:3] if self.patient: return getattr(self.patient, field) else: return getattr(self, field) @fields.depends('sex', 'patient') def on_change_with_childbearing_age(self, *a, **k): if self.patient: return self.patient.childbearing_age else: if self.sex == 'm': return False else: return True @classmethod def get_medical_alert(cls, instances, name): out = [(i.id, i.priority == MED_ALERT) for i in instances] return dict(out) @classmethod def set_medical_alert(cls, instances, name, value): to_write = [] if value is False: return for i in instances: if i.priority > MED_ALERT: to_write.append(i) cls.write(to_write, {'priority': MED_ALERT}) @classmethod def get_do_details_perm(cls, instances, name): user_has_perm = get_model_field_perm(cls.__name__, name, 'create', default_deny=False) outval = dict([(x.id, user_has_perm) for x in instances]) return outval @staticmethod def default_can_do_details(): user_has_perm = get_model_field_perm('gnuhealth.triage.entry', 'can_do_details', 'create', default_deny=False) return user_has_perm @staticmethod def default_childbearing_age(): return True @staticmethod def uri_selection(): return [(None, '')] + URINALYSIS['default'] @staticmethod def uri_nitrite_selection(): return [(None, '')] + URINALYSIS['nitrite'] def get_first_time_contact(self, name): '''This method gets the date and time this person was first made contact with by the attending staff''' return localtime(self.create_date).strftime('%F %T') @staticmethod def default_institution(): HI = Pool().get('gnuhealth.institution') return HI.get_institution() def get_note_display(self, name): notes = [] if self.note_entries: return u'\n---\n'.join( map(lambda x: u' :\n'.join([x.byline, x.note]), self.note_entries)) else: return '' @classmethod @ModelView.button_action('health_triage_queue.act_triage_referral_starter') def go_referral(cls, queue_entries): pass @classmethod @ModelView.button def set_done(cls, entries): '''set done=True on the triage entry''' save_data = {'done': True} for entry in entries: if not entry.end_time: cls.raise_user_warning( 'triage_end_date_warn1', 'End time has not been set.\nThe current Date and time ' 'will be used.') save_data.update(end_time=datetime.now()) cls.write(entries, save_data)
class Mandate(Workflow, ModelSQL, ModelView): 'SEPA Mandate' __name__ = 'account.payment.sepa.mandate' party = fields.Many2One('party.party', 'Party', required=True, select=True, states={ 'readonly': Eval('state').in_( ['requested', 'validated', 'canceled']), }, depends=['state']) account_number = fields.Many2One( 'bank.account.number', 'Account Number', ondelete='RESTRICT', states={ 'readonly': Eval('state').in_(['validated', 'canceled']), 'required': Eval('state') == 'validated', }, domain=[ ('type', '=', 'iban'), ('account.owners', '=', Eval('party')), ], depends=['state', 'party']) identification = fields.Char('Identification', size=35, states={ 'readonly': Eval('identification_readonly', True), 'required': Eval('state') == 'validated', }, depends=['state', 'identification_readonly']) identification_readonly = fields.Function( fields.Boolean('Identification Readonly'), 'get_identification_readonly') company = fields.Many2One( 'company.company', 'Company', required=True, select=True, domain=[ ('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1)), ], states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) type = fields.Selection([ ('recurrent', 'Recurrent'), ('one-off', 'One-off'), ], 'Type', states={ 'readonly': Eval('state').in_(['validated', 'canceled']), }, depends=['state']) sequence_type_rcur = fields.Boolean("Always use RCUR", states={ 'invisible': Eval('type') == 'one-off', }, depends=['type']) scheme = fields.Selection([ ('CORE', 'Core'), ('B2B', 'Business to Business'), ], 'Scheme', required=True, states={ 'readonly': Eval('state').in_(['validated', 'canceled']), }, depends=['state']) scheme_string = scheme.translated('scheme') signature_date = fields.Date('Signature Date', states={ 'readonly': Eval('state').in_( ['validated', 'canceled']), 'required': Eval('state') == 'validated', }, depends=['state']) state = fields.Selection([ ('draft', 'Draft'), ('requested', 'Requested'), ('validated', 'Validated'), ('canceled', 'Canceled'), ], 'State', readonly=True) payments = fields.One2Many('account.payment', 'sepa_mandate', 'Payments') has_payments = fields.Function(fields.Boolean('Has Payments'), 'has_payments') @classmethod def __setup__(cls): super(Mandate, cls).__setup__() cls._transitions |= set(( ('draft', 'requested'), ('requested', 'validated'), ('validated', 'canceled'), ('requested', 'canceled'), ('requested', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('state').in_(['requested', 'validated']), 'depends': ['state'], }, 'draft': { 'invisible': Eval('state') != 'requested', 'depends': ['state'], }, 'request': { 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'validate_mandate': { 'invisible': Eval('state') != 'requested', 'depends': ['state'], }, }) # t = cls.__table__() # JMO/RSE 2017_04_18 Following 4929e02594d # We override in coog the possibility to keep the mandate # for several bank account but we suffer from the register order # if we try to delete the constraint from coog module # t = cls.__table__() # cls._sql_constraints = [ # ('identification_unique', Unique(t, t.company, t.identification), # 'account_payment_sepa.msg_mandate_unique_id'), # ] @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_type(): return 'recurrent' @classmethod def default_sequence_type_rcur(cls): return False @staticmethod def default_scheme(): return 'CORE' @staticmethod def default_state(): return 'draft' @staticmethod def default_identification_readonly(): pool = Pool() Configuration = pool.get('account.configuration') config = Configuration(1) return bool(config.sepa_mandate_sequence) def get_identification_readonly(self, name): return bool(self.identification) def get_rec_name(self, name): if self.identification: return self.identification return '(%s)' % self.id @classmethod def search_rec_name(cls, name, clause): return [tuple(('identification', )) + tuple(clause[1:])] @classmethod def create(cls, vlist): pool = Pool() Sequence = pool.get('ir.sequence') Configuration = pool.get('account.configuration') config = Configuration(1) vlist = [v.copy() for v in vlist] for values in vlist: if (config.sepa_mandate_sequence and not values.get('identification')): values['identification'] = Sequence.get_id( config.sepa_mandate_sequence.id) # Prevent raising false unique constraint if values.get('identification') == '': values['identification'] = None return super(Mandate, cls).create(vlist) @classmethod def write(cls, *args): actions = iter(args) args = [] for mandates, values in zip(actions, actions): # Prevent raising false unique constraint if values.get('identification') == '': values = values.copy() values['identification'] = None args.extend((mandates, values)) super(Mandate, cls).write(*args) @classmethod def copy(cls, mandates, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('payments', []) default.setdefault('signature_date', None) default.setdefault('identification', None) return super(Mandate, cls).copy(mandates, default=default) @property def is_valid(self): if self.state == 'validated': if self.type == 'one-off': if not self.has_payments: return True else: return True return False @property def sequence_type(self): if self.type == 'one-off': return 'OOFF' elif not self.sequence_type_rcur and (not self.payments or all( not p.sepa_mandate_sequence_type for p in self.payments) or all(p.rejected for p in self.payments)): return 'FRST' # TODO manage FNAL else: return 'RCUR' @classmethod def has_payments(cls, mandates, name): pool = Pool() Payment = pool.get('account.payment') payment = Payment.__table__ cursor = Transaction().connection.cursor() has_payments = dict.fromkeys([m.id for m in mandates], False) for sub_ids in grouped_slice(mandates): red_sql = reduce_ids(payment.sepa_mandate, sub_ids) cursor.execute(*payment.select(payment.sepa_mandate, Literal(True), where=red_sql, group_by=payment.sepa_mandate)) has_payments.update(cursor.fetchall()) return {'has_payments': has_payments} @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, mandates): pass @classmethod @ModelView.button @Workflow.transition('requested') def request(cls, mandates): pass @classmethod @ModelView.button @Workflow.transition('validated') def validate_mandate(cls, mandates): pass @classmethod @ModelView.button @Workflow.transition('canceled') def cancel(cls, mandates): # TODO must be automaticaly canceled 13 months after last collection pass @classmethod def delete(cls, mandates): for mandate in mandates: if mandate.state not in ('draft', 'canceled'): raise AccessError( gettext( 'account_payment_sepa' '.msg_mandate_delete_draft_canceled', mandate=mandate.rec_name)) super(Mandate, cls).delete(mandates)
class Template: __metaclass__ = PoolMeta __name__ = 'product.template' customs_category = fields.Many2One('product.category', 'Customs Category', domain=[ ('customs', '=', True), ], states={ 'required': Eval('tariff_codes_category', False), }, depends=['tariff_codes_category']) tariff_codes_category = fields.Boolean( "Use Category's Tariff Codes", help='Use the tariff codes defined on the category') tariff_codes = fields.One2Many( 'product-customs.tariff.code', 'product', 'Tariff Codes', order=[('sequence', 'ASC'), ('id', 'ASC')], states={ 'invisible': ((Eval('type') == 'service') | Eval('tariff_codes_category', False)), }, depends=['type', 'tariff_codes_category']) @classmethod def __register__(cls, module_name): TableHandler = backend.get('TableHandler') cursor = Transaction().connection.cursor() pool = Pool() Category = pool.get('product.category') sql_table = cls.__table__() category = Category.__table__() table = TableHandler(cls, module_name) category_exists = table.column_exist('category') super(Template, cls).__register__(module_name) # Migration from 3.8: duplicate category into account_category if category_exists: # Only accounting category until now cursor.execute(*category.update([category.customs], [True])) cursor.execute(*sql_table.update([sql_table.customs_category], [sql_table.category])) @classmethod def default_tariff_codes_category(cls): return False def get_tariff_code(self, pattern): if not self.tariff_codes_category: for link in self.tariff_codes: if link.tariff_code.match(pattern): return link.tariff_code else: return self.customs_category.get_tariff_code(pattern) @classmethod def view_attributes(cls): return super(Template, cls).view_attributes() + [ ('//page[@id="customs"]', 'states', { 'invisible': Eval('type') == 'service', }), ] @classmethod def delete(cls, templates): pool = Pool() Product_TariffCode = pool.get('product-customs.tariff.code') products = [str(t) for t in templates] super(Template, cls).delete(templates) for products in grouped_slice(products): product_tariffcodes = Product_TariffCode.search([ 'product', 'in', list(products), ]) Product_TariffCode.delete(product_tariffcodes)