class Adjustment(models.TransientModel): _name = 'xopgi.unrealized_gl_adjustment' wizard = fields.Many2one('xopgi.unrealized_gl_wizard') account = fields.Many2one('account.account') account_name = fields.Char(related="account.name") account_code = fields.Char(related="account.code") account_currency = fields.Many2one(related="account.currency_id") foreign_balance = fields.Float(compute='_compute_all', default=0) balance = fields.Float(compute='_compute_all', default=0) adjusted_balance = fields.Float(compute='_compute_all', default=0) gainloss = fields.Float(compute='_compute_all', default=0) @api.depends('account', 'wizard') def _compute_all(self): precision = DecimalPrecision.precision_get('Account') company_currency = self.env.user.company_id.currency_id # Map records to accounts so that we can compute the balances in a single # DB query account_map = dict(zip(self.mapped('account.id'), self)) assert len(account_map) == len(self) close_date = self[0].wizard.close_date tables, where_clause, where_params = AccountMoveLine.with_context( state='posted', date_to=close_date)._query_get() if not tables: tables = '"account_move_line"' if where_clause.strip(): filters = [where_clause] else: filters = [] filters.append('"account_move_line"."account_id" IN %s') where_params.append(tuple(account_map.keys())) query = (''' SELECT account_id AS id, COALESCE(SUM(debit), 0) - COALESCE(SUM(credit), 0) AS balance, COALESCE(SUM(amount_currency), 0) as foreign_balance FROM {tables} WHERE {filters} GROUP BY account_id ''').format(tables=tables, filters=' AND '.join(filters)) self.env.cr.execute(query, where_params) for row in self.env.cr.dictfetchall(): record = account_map.pop(int( row['id'])) # cast to int, otherwise KeyError account = record.account record.balance = balance = row['balance'] record.foreign_balance = row['foreign_balance'] record.adjusted_balance = adjusted = account.currency_id.with_context( date=close_date).compute( record.foreign_balance, company_currency, round=False, ) record.gainloss = round(adjusted - balance, precision) for record in account_map.values(): record.balance = record.foreign_balance = 0 record.adjusted_balance = record.gainloss = 0
class RecurrentEvent(models.Model): _name = 'cdr.recurrent.event' _description = "Recurrent CDR event" _inherits = { 'cdr.system.event': 'event_id', 'cdr.recurrent.event.def': 'recurrence_def' } event_id = fields.Many2one('cdr.system.event', required=True, ondelete='cascade') time = fields.Float() recurrence_def = fields.Many2one('cdr.recurrent.event.def', required=True, ondelete='cascade') def update_event(self, value): '''Update the fields next call, state and action for an event. When an event is evaluated is necessary to update its values. ''' next_call = self.recurrence_def.next_date(self.recurrence_def.rrule) state = 'raising' if value else 'not_raising' action = 'raise' if state == 'raising' else 'do_nothing' values = dict(next_call=next_call, state=state, action=action) self.write(values) def evaluate(self, cycle): '''Evaluate the recurrent event in a evaluation cycle. ''' if isinstance(cycle, int): cycle = self.env['cdr.evaluation.cycle'].browse(cycle) try: value = self.event_id._evaluate() except Exception: logger.exception('Error evaluating event %s defined as: ', self.name, self.definition) return None else: self.update_event(value) @api.model @api.returns('self', lambda value: value.id) def create(self, vals): if any(field in self.recurrence_def._fields for field in vals): vals.update(is_recurrent=True) return super(RecurrentEvent, self).create(vals)
class A(models.Model): _name = 'model.a' add_char = fields.Char() add_text = fields.Text() price_int = fields.Integer() cost_float = fields.Float() min_int = fields.Integer() max_int = fields.Integer() active = fields.Boolean(default=True) parent_id = fields.Many2one('model.a', 'Parent') meldb_ids = fields.Many2many(comodel_name='model.b', column1='meld_column2_id', column2='meld_column1_id', string='relation')
class BasicEvent(models.Model): _name = 'cdr.basic.event' _description = 'Basic CDR event' _inherits = {'cdr.system.event': 'event_id'} event_id = fields.Many2one('cdr.system.event', required=True, ondelete='cascade') interval = fields.Float( required=True, help='Time (in hours:minutes format) between evaluations.') time_to_wait = fields.Float( required=True, help='Time (in hours:minutes format) getting ' 'consecutive positive evaluations before raise.') times_to_raise = fields.Integer( help='Waiting time to launch an event while an evidence is true in a ' 'time interval') @api.depends('interval') def get_next_call(self): '''Compute the next evaluation date. ''' for event in self: if event.active and event.interval: event.next_call = datetime.now() + timedelta( hours=event.interval) else: event.next_call = False def update_event(self, value, cycle): '''Update the fields next call, state and action for an event. When an event is evaluated is necessary to update its values. ''' next_call = str2dt(cycle.create_date) + timedelta(hours=self.interval) # If the interval is less or equal zero that means that the event does # not wait any time to launch. if self.interval <= 0: times_to_raise = -1 else: times_to_raise = ((self.time_to_wait / self.interval) if not value else self.times_to_raise - 1) state = 'raising' if value and times_to_raise < 1 else 'not_raising' if self.state == 'raising': action = 'continue_raising' if state == 'raising' else 'stop_raising' else: action = 'raise' if state == 'raising' else 'do_nothing' values = dict(next_call=next_call, state=state, action=action) self.write(values) def evaluate(self, cycle): '''Evaluate the basic event in a evaluation cycle. ''' if isinstance(cycle, int): cycle = self.env['cdr.evaluation.cycle'].browse(cycle) try: value = self.event_id._evaluate() except Exception: logger.exception('Error evaluating event %s defined as: ', self.name, self.definition) return None else: self.update_event(value, cycle)
class AccountAnalyticAccount(models.Model): _inherit = 'account.analytic.account' # The formulae for the commission to salesmen is based on a simple # projection from the desired sales margin (required_margin, max_margin) # to the commission margins salesmen earn per sale. # # The projection is simply a geometrical projection: required_margin # yields the minimal commission margin, the max_margin yields the maximal # commission margin. Accounts that fail to achieve the required_margin # yield 0% commission, accounts that surpass the max_margin yields just # the maximal commission margin. # # The projection follows the scheme below: # # min margin ----------X------------------ max margin # | # min com ----------V------------------ max comm # # X represents the actual margin, V represents the resultant commission # margin. # # V = min_comm * alpha + max_comm * (1 - alpha), where alpha is given by # the formula: # # alpha = (max_margin - X)/(max_margin - min_margin) # # if alpha < 0: alpha = 0 # Too much margin # # if V < min_comm: V = 0 # # Each account may have any desired required_margin and max_margin, # provided the required_margin is greater or equal of is parent account # and the max_margin is less or equal than the parent account. # # The account may have any desired min commission and max commission, no # restrictions are placed. # # By default, accounts take all these parameters from its parent. So it's # wise to only establish them at accounts that consolidate operational # branches. # required_margin = fields.Float( string='Required margin', help=('This is the minimum margin required for an operation. If 0 ' 'no commission will be calculated.'), required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) max_margin = fields.Float( string='Maximum margin', help='This is maximum margin allowed for an operation.', required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) min_commission_margin = fields.Float( string='Minimum commission margin', help='This is minimum margin for commissions.', required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) max_commission_margin = fields.Float( string='Maximum commission margin', help='This is maximum margin for commissions.', required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) current_required_margin = fields.Float( compute=_compute_from_branch('required_margin', 'current_required_margin', default=0), digits=dp.get_precision('Account'), ) current_max_margin = fields.Float( compute=_compute_from_branch('max_margin', 'current_max_margin', default=0), digits=dp.get_precision('Account'), ) current_min_comm = fields.Float( compute=_compute_from_branch('min_commission_margin', 'current_min_comm', default=0), digits=dp.get_precision('Account'), ) current_max_comm = fields.Float( compute=_compute_from_branch('max_commission_margin', 'current_max_comm', default=0), digits=dp.get_precision('Account'), ) percentage_margin = fields.Float( string='Margin %', help='Percentage margin related to credit.', compute='_compute_commission', digits=dp.get_precision('Account'), ) percentage_commission = fields.Float( string='Commission %', help='Percentage commission related to profit.', compute='_compute_commission', digits=dp.get_precision('Account'), ) commission = fields.Float( string='Commission', help='Commission related to profit.', compute='_compute_commission', digits=dp.get_precision('Account'), ) invoiced = fields.Float( string='Invoiced', help=('The amount invoiced for this account, ' 'discounting refunds.'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) expended = fields.Float( string='Expended', help=('The amount expended for this account, ' 'discounting refunds.'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) amount_undefined = fields.Float( string='Undefined', help=('Total that cannot be accounted as invoiced or expended because ' 'it is not attached to an invoice'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) self_balance = fields.Float( string='Balance', help=('Self balance'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) primary_salesperson_id = fields.Many2one( "res.users", string="Salesperson", help="Primary salesperson in operation", compute="_compute_primary_salesperson", store=True ) supplier_invoice_id = fields.Many2one('account.invoice', ondelete='set null') # TODO: Ensure only groups='base.group_sale_manager' can update the # commission margins. So far, only the goodwill of ignorance may save us. @api.depends('line_ids') def _compute_invoiced(self): for record in self: invoiced = expended = undefined = 0 for line in record.line_ids: if line.move_id and line.move_id.invoice_id: if line.move_id.invoice_id.type in INCOME_INVOICE_TYPES: invoiced += line.amount else: expended -= line.amount else: undefined += line.amount record.invoiced = invoiced record.expended = expended record.amount_undefined = undefined record.self_balance = invoiced - expended + undefined @api.depends('invoiced', 'self_balance') def _compute_commission(self): for record in self: margin, comm_percent, balance = _compute_margin_commission(record) record.percentage_margin = margin * 100 record.percentage_commission = comm_percent * 100 record.commission = comm_percent * balance @api.depends('line_ids.move_id.invoice_id.user_id') def _compute_primary_salesperson(self): for account in self: if not account.active: account.primary_salesperson_id = False else: user_id = next( (line.move_id.invoice_id.user_id for line in account.line_ids if line and line.move_id and line.move_id.invoice_id if line.move_id.invoice_id.type in INCOME_INVOICE_TYPES if line.move_id.invoice_id.user_id), None ) if user_id: account.primary_salesperson_id = user_id else: account.primary_salesperson_id = False @api.constrains('required_margin', 'max_margin') def _validate_margins(self): for record in self: required_margin = record.required_margin max_margin = record.max_margin if bool(required_margin) != bool(max_margin): # Either both are set or both are unset raise ValidationError('You must set both required_margin ' 'and max_margin or none.') if required_margin < 0 or required_margin > 100: raise_validation_error('required minimum margin') if max_margin < 0 or max_margin > 100: raise_validation_error('maximum allowed margin') if required_margin and max_margin and \ required_margin >= max_margin: raise ValidationError('required margin must be less that ' 'max margin.') parent = record.parent_id if parent and parent.id: if parent.required_margin and required_margin and \ required_margin < parent.required_margin: raise ValidationError( 'You cannot lower the required margin') if parent.max_margin and max_margin and \ max_margin > parent.max_margin: raise ValidationError( 'You cannot raise the maximum margin') # TODO: If the children enter in violation what to do? # for child in record.complete_child_ids: # child._validate_margins() @api.constrains('min_commission_margin', 'max_commission_margin') def _validate_commission_margins(self): for record in self: min_comm = record.min_commission_margin max_comm = record.max_commission_margin if bool(min_comm) != bool(max_comm): # Either both are set or both are unset raise ValidationError('You must set both min commission ' 'and max commission or none.') if min_comm < 0 or min_comm > 100: raise_validation_error('minimum commission margin') if max_comm < 0 or max_comm > 100: raise_validation_error('maximum commission margin') if min_comm and max_comm and min_comm >= max_comm: raise ValidationError('min commission must be less that ' 'max commission.')
class RecurrentModel(models.AbstractModel): '''A mixin for recurrent things (in time). This mixin modifies `search` so that your model can be seen as an occurrence of the recurrent event. You must provide `search_occurrences` (or the deprecated `virtual_id`) context key to True to trigger this behavior. The actual recurrence model is that implemented by the python module `dateutil.rrule`:mod:. Examples:: class MyMeeting(Model): _name = 'my.meeting' _inherit = [_name, 'recurrent.model'] >>> Meeting = self.env['my.meeting'] # Case 1:Repeat once every day >>> meeting = Meeting.create(dict( ... interval=1, ... freq='daily', ... end_type='no_end_date' ... )) # Case 2:Repeat each week on Mondays and Fridays >>> meeting = Meeting.create(dict( ... interval=1, ... freq='weekly', ... mo=True, ... fr=True, ... end_type='no_end_date' ... )) # Case 3:Repeat each month the nth days Mondays and Fridays': >>> meeting = Meeting.create(dict( ... interval=1, ... freq='monthly', ... days_option='week_day', ... mo=True, ... fr=True, ... end_type='no_end_date' ... )) # Case 4:Repeat each month the cardinal days of month: >>> meeting = Meeting.create(dict( ... interval=1, ... freq='monthly', ... days_option='month_day', ... monthly_day=5, ... end_type='no_end_date', ... end_type='no_end_date' ... )) # Repeat each year, similar to case three specifying that month of the # year. >>> meeting = Meeting.create(dict( ... interval=1, ... freq='yearly', ... days_option='month_day', ... monthly_day=5, ... months=1, ... end_type='no_end_date' ... )) # Repeat each year specifying number of days before western easter # sunday. >>> meeting = Meeting.create(dict( ... interval=1, ... freq='yearly', ... is_easterly=True, ... byeaster_day=-2, ... end_type='no_end_date' ... )) :attr months: specify year month. :attr monthly_day: Refers to the nth day of the month. :attr byeaster: Number of days from western easter sunday by default is 0. ''' _name = RECURRENT_MIXIN_MODEL _inherit = [RECURRENT_DESCRIPTION_MODEL] _description = 'Recurrent model' date_to = fields.Date( 'Final date', help='End date of the occurrence' ) @api.multi def _get_date(self): '''Get the values for the compute 'date' field of the recurrent object using the user timezone. This is calculated with the field 'date_from'. ''' for item in self: # Plus an hour to avoid the problem with daylight saving time. date_from = normalize_dt(item.date_from) + timedelta(hours=1) if isinstance(item.id, string_types): start, _ = self._extract_dates(item.id, item.calendar_duration or 23) item.date = normalize_dt_str(start) else: item.date = normalize_dt_str(date_from) def _search_date(self, operator, value): """Get the domain to search for 'date'. We simply translate it to search by 'date_from'. """ if value not in (False, None): # Some modules try to see if the date is not set by passing False. value = normalize_date(value) return [('date_from', operator, value)] @api.multi def _get_duration(self): '''Get the values on hours to the compute field 'calendar_duration' of recurrent object. ''' for item in self: # By default 'duration = 1 day' if item.duration: # Minus 2 to avoid problems with daylight savings. This allows # to have instances from 1AM to 11PM in case change of use # schedule is still on the same day. item.calendar_duration = item.duration * _HOURS_PER_DAY - 2 elif item.date_to: df = normalize_date(item.date_from) dt = normalize_date(item.date_to) item.calendar_duration = (dt - df).total_seconds() / _SECS_PER_HOUR else: item.calendar_duration = 1 date = fields.Datetime( compute=_get_date, search=_search_date, method=True, string='Date and time for representation in calendar views', ) calendar_duration = fields.Float( compute='_get_duration', method=True, string='Duration', help='Get the duration on hours of the recurrence' ) active = fields.Boolean( 'Active', default=True, help='Indicate if recurrent object is active or not' ) is_recurrent = fields.Boolean( 'Is recurrent', default=True, help='Check if is recurrent.' ) @api.model @api.returns('self', downgrade=_SEARCH_DOWNGRADE) def search(self, args, offset=0, limit=None, order=None, count=False): """If 'virtual_id' not in context or are False then normally search, else search virtual occurrences and return then. """ virtual_id = self._context.get('virtual_id', False) if virtual_id and not count: return self._logical_search(args, offset=offset, limit=limit, order=order) else: return self._real_search(args, offset=offset, limit=limit, order=order, count=count) @api.model def _real_search(self, args, offset=0, limit=None, order=None, count=False): return super(RecurrentModel, self).search( args, offset=offset, limit=limit, order=order, count=count ) @api.model def _logical_search(self, domain, offset=0, limit=None, order=None): '''Finds all occurrences that match the domain. Since there can be an unlimited number of occurrences, `limit` is always bounded to MAX_OCCURRENCES. ''' import operator from xoutil.future.functools import reduce from xoutil.future.itertools import ifilter, first_n, map from xoutil.eight import integer_types from xoutil.fp.tools import compose from ..tools.predicate import Predicate # Normalize limit to be 0 <= limit <= MAX_OCCURRENCES. If limit is # zero, bail the whole procedure by returning the empty recordset limit = min(max(limit or 0, 0), MAX_OCCURRENCES) if not limit: return self.browse() # This is a tricky implementation to get right. Domain can be # arbitrarily complex, and mix date-related and non-date related # conditions. Since all non-date related conditions will match for # both the occurrences and the recurrent pattern itself, we'd like to # find the *recurrence patterns* that match the domain **striping the # date-related conditions**, and then rematch the occurrences with the # rest of the date-related domain. # # This is to say that if we're given a domain T that may contain # date-related conditions, we would like to find another domain Q # without date-related conditions such that ``T => Q``. # # Then we could find recurrence patterns that match Q, and afterwards # filter occurrences by T. This would make the amount of filtering # done in Python less of an issue. # # Trivially, for any T, making ``Q=[]`` (i.e always True) satisfies # this condition, but then will have to filter all of the recurrence # in Python and that might be a performance issue. # # Yet, trying to find a non-trivial Q cannot be achieved in all cases. # # In the following, all capitalized symbols starting with D will # represent predicates (terms in a domain) involving the `date` field, # non capitalized symbols will represent predicates not involving the # `date` field. The logical AND and OR will be represented by `and` # and `or`, where the logical NOT will be `~`. # # For the predicate T ``(D or ~n) and (~D or n)``, Q can be ``~n or # n``, which is the same as ``[]``. For the predicate ``(D or n) and # (~D or n)``, we can find Q as ``n``. Notice that both predicates # are in Conjunctive Normal Form (CNF). # # For Odoo domains, we can use the 2nd normal form (2NF) which # distributes NOT within the term themselves, this mean we won't see # any ``~`` in our predicates. So a CNF can be ``(D or nn) and (DD or # n)`` that will yield Q to be ``~nn or ~n``, which *we* (but not our # machinery) know to be equivalent to ``[]``. # # If the domain is given in CNF and 2NF, it's easy to find Q. So, the # algorithm needed is to find the CNF for the 2NF of a domain. # # HOWEVER, I believe that this is NOT currently needed IN OUR CASE. # So let's mark it as a thing to do. # # TODO: Compute the CNF to get Q. # res = self._real_search([], order=order) # At this point `res` has the recurrence in the order requested. # _iter_occurrence will yield the occurrences in the `date` order # first. assert all(isinstance(r.id, integer_types) for r in res) pred = Predicate(domain) occurrences = first_n(ifilter(pred, map( compose(self.browse, self._real_id2virtual), res._iter_occurrences_dates() )), limit) return reduce(operator.or_, occurrences, self.browse()) @api.multi def read(self, fields=None, load='_classic_read'): '''Return the list of dict with the [fields]. If [ids] contain virtual ids read data from real ids and create virtual copies to return If `fields` have [date, date_from, date_to] return a list of dict with this fields plus id. If :param calendar_duration: is include in fields it will be add to the result else is exclude. ''' if not fields: fields = [] fields2 = fields[:] if fields else [] targets = ('date', 'date_from', 'date_to') has_dates = any(field in fields for field in targets) if fields and 'calendar_duration' not in fields and has_dates: fields2.append('calendar_duration') ids_index = {record.id: int(self._virtual_id2real(record.id)) for record in self} # If dict:ids_index have as keys virtual_ids:(str,unicode) the # id_reals are dict.values() else the id_reals are dict.keys() ids_reals = list(set(ids_index.values())) reals = self.browse(ids_reals) values = super(RecurrentModel, reals).read(fields2, load=load) results = {r['id']: r for r in values} def select(virtual_id, data): if data is None: return None else: if isinstance(virtual_id, string_types): date_show, _ = self._extract_dates(virtual_id, 23) data['id'] = virtual_id data['date'] = date_show return data return list(filter(bool, ( select(key, results.get(ids_index[key], None)) for key in ids_index ))) @api.multi def unlink(self): # TODO: implement the logic for rule exceptions and one occurrence # modifications. self._check_for_one_occurrency_change() return super(RecurrentModel, self).unlink() @api.multi def write(self, values): # TODO: implement the logic for rule exceptions and one occurrence # modifications. self._check_for_one_occurrency_change() res = super(RecurrentModel, self).write(values) if 'no_update_rrule' not in self._context and not values.get('rrule'): self._update_rrule() return res @api.model def create(self, values): recurrence = super(RecurrentModel, self).create(values) if recurrence: recurrence._update_rrule() return recurrence @api.multi def _iter_occurrences_dates(self, start=None): '''Produce **all** the occurrences. Each item is a pair of `(date, record)`, where `date` is the date of an occurrence defined by `record. Records are all the records in `self`. `date` is always a `datetime`:class: object. Items are produced from `start` to end in order of occurrence. Thus, items from different records can be intertwined. If `start` is None, use the first possible date (`date_from`) for all items. If any record in `self` is not recurrent it will yield a single instance with `date` equal to `date_from`. .. note:: This can be a potentially infinite iterator. ''' from xoutil.future.itertools import merge def _iter_from(record): if record.is_recurrent: for date in record.iter_from(start=start): # The date comes first so that `merge` result be sorted by # date. yield date, record else: yield normalize_dt(record.date_from), record return merge(*(_iter_from(record) for record in self)) @api.model def _real_id2virtual(self, recurrent_date, real_id): if real_id and recurrent_date: recurrent_date = normalize_dt(recurrent_date) recurrent_date = recurrent_date.strftime(_V_DATE_FORMAT) return '%d-%s' % (real_id, recurrent_date) return real_id @api.model def _virtual_id2real(self, virtual_id=None): from xoutil.eight import string_types if virtual_id and isinstance(virtual_id, string_types): res = virtual_id.split('-', 1) if len(res) >= 2: real_id = res[0] return real_id return virtual_id @api.model def _extract_dates(self, virtual_id, duration): '''Extract start and end dates from the virtual id and duration.''' _, res = virtual_id.split('-', 1) start = datetime.strptime(res, _V_DATE_FORMAT) end = start + timedelta(hours=duration) start = normalize_dt_str(start) end = normalize_dt_str(end) return start, end @api.multi def _update_rrule(self): '''Update the rrule string representation.''' for record in self: record.rrule = str(record.get_rrule_from_description()) @api.model def _check_for_one_occurrency_change(self): 'Ensure only real ids.' real_ids = {self._virtual_id2real(x) for x in self.ids} if real_ids != set(self.ids): raise ValidationError('Expected only real ids') @api.multi def is_occurrency(self, day): '''Get True if day(date) is on one of virtuals ocurrences of real [ids] else False :param ids: real ids where search :param day: date to search :return: True or False ''' day = datetime_user_to_server_tz(self._cr, self._uid, normalize_dt(day), self._context.get('tz')) day_str = normalize_dt_str(day.replace(hour=23, minute=59)) res_ids = self.search([('id', 'in', self._ids), ('date', '<=', day_str)]) for data in res_ids.read(['date', 'calendar_duration']): d_from = normalize_dt(data['date']) d_to = d_from + timedelta(hours=data.get('calendar_duration')) if d_from.date() <= day.date() <= d_to.date(): return True return False
class UnrealizedGLWizard(models.TransientModel): _name = "xopgi.unrealized_gl_wizard" def _get_valid_currencies(self): accounts = _get_valid_accounts() currencies = {account.currency_id.id for account in accounts} return [("id", "in", list(currencies))] def _get_currency(self): return Currency.search(self._get_valid_currencies(), limit=1) def _get_journal(self): return self.env.user.company_id.ugl_journal_id def _get_gain_account(self): return self.env.user.company_id.ugl_gain_account_id def _get_loss_account(self): return self.env.user.company_id.ugl_loss_account_id def _get_default_adjustments(self): currency = self._get_currency() accounts = _get_valid_accounts(currency) adjustments = [] for account in accounts: with get_creator(self.env['xopgi.unrealized_gl_adjustment']) as c: c.update(account=account.id, wizard=self.id) adjustments.append(c.result) return adjustments close_date = fields.Date(string="Close Date", required=True, default=fields.Date.today) currency_id = fields.Many2one("res.currency", "Currency", required=True, default=_get_currency, domain=_get_valid_currencies) currency_rate = fields.Float(string="Currency Rate", readonly=True, digits=(15, 9), compute='_compute_all') journal_id = fields.Many2one("account.journal", string="Journal", required=True, domain=GENERAL_JOURNAL_DOMAIN, default=_get_journal, readonly=True) gain_account_id = fields.Many2one("account.account", string="Gain Account", required=True, domain=REGULAR_ACCOUNT_DOMAIN, default=_get_gain_account, readonly=True) loss_account_id = fields.Many2one("account.account", string="Loss Account", required=True, domain=REGULAR_ACCOUNT_DOMAIN, default=_get_loss_account, readonly=True) adjustments = fields.One2many( 'xopgi.unrealized_gl_adjustment', 'wizard', compute='_compute_all', ) @api.depends('close_date', 'currency_id') def _compute_all(self): company_currency = self.env.user.company_id.currency_id for record in self: if any(record.currency_id): currency = record.currency_id.with_context( date=self.close_date) record.currency_rate = currency.compute( 1, company_currency, round=False, ) accounts = _get_valid_accounts(record.currency_id) adjustments = [ CREATE_RELATED(account=account, wizard=record) for account in accounts ] record.adjustments = adjustments @api.multi def generate(self): from xoeuf.models.extensions import get_treeview_action moves = self._do_generate() return get_treeview_action(moves) @api.multi def _do_generate(self): moves = Move.browse() for adjustment in self.adjustments: gainloss = adjustment.gainloss if gainloss: sequence = self.journal_id.sequence_id account = adjustment.account name = 'UGL: %s' % self.close_date ref = 'UGL for AC: %s-%s at %s' % (account.code, account.name, self.close_date) with get_creator(Move) as creator: creator.update( name=next_seq(sequence), ref=ref, journal_id=self.journal_id.id, date=self.close_date, ) if gainloss > 0: creator.create(LINES_FIELD_NAME, name=name, account_id=self.gain_account_id.id, credit=gainloss) creator.create(LINES_FIELD_NAME, name=name, account_id=account.id, debit=gainloss, amount_currency=0, currency_id=account.currency_id.id) else: creator.create(LINES_FIELD_NAME, name=name, account_id=self.loss_account_id.id, debit=-gainloss) creator.create(LINES_FIELD_NAME, name=name, account_id=account.id, credit=-gainloss, amount_currency=0, currency_id=account.currency_id.id) # outside the with we get the result moves |= creator.result return moves