import calendar from datetime import date, datetime from odoo import api, fields, models QUARTER_NAMES = {1: 'первый', 4: 'второй', 7: 'третий', 10: 'четвертый'} class CommContract(models.Model): _name = 'comm.contract' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Договор аренды' _order = 'name' name = fields.Char(string='Номер договора', tracking=True) date_start = fields.Date(string='Дата начала действия') date_end = fields.Date(string='Дата окончания действия') building_id = fields.Many2one('comm.building', string='Объект') state = fields.Selection( [ ('draft', 'Черновик'), ('open', 'Действует'), ('closed', 'Не действует'), ], string='Статус', default='draft', tracking=True, ) partner_id = fields.Many2one('res.partner', string='Заказчик', required=True) commercial_partner_id = fields.Many2one( 'res.partner', string='Коммерческий партнёр', store=True, compute='_compute_commercial_partner_id', ) line_ids = fields.One2many('comm.contractline', 'contract_id', string='Услуги') building = fields.Char(string='Название объекта') credit = fields.Float(string='Долг', compute='_compute_credit') inv_count = fields.Integer(string='Количество счетов', compute='_compute_inv_count') company_id = fields.Many2one( 'res.company', string='Компания', required=True, default=lambda self: self.env.company, ) contract_id = fields.Many2one( 'partner.contract.customer', string='Связанный фин. договор', ) day_inv = fields.Integer(string='День счёта, 0 если автомат', default=0) day_upd = fields.Integer(string='День УПД, 0 если автомат', default=0) @api.depends('partner_id', 'partner_id.parent_id') def _compute_commercial_partner_id(self): for rec in self: rec.commercial_partner_id = ( rec.partner_id.parent_id or rec.partner_id ) def _compute_credit(self): for rec in self: rec.credit = rec.partner_id.credit if rec.partner_id else 0.0 def _compute_inv_count(self): Move = self.env['account.move'] for rec in self: rec.inv_count = Move.search_count([ ('commercial_partner_id', '=', rec.commercial_partner_id.id), ('year', '>', 0), ('state', 'not in', ['cancel']), ('comm_contract_id', '=', rec.id), ]) def connect_counters(self): """Привязать счётчики строк договора к самому договору.""" for contract in self.env['comm.contract'].search([]): for line in contract.line_ids: if line.counter_id: line.counter_id.contract_id = contract # ------------------------------------------------------------------ # Формирование счетов # ------------------------------------------------------------------ def _is_period_due(self, line, month): """Наступил ли период начисления для строки в данном месяце.""" if line.period == 'month': return True if line.period == 'quarter': return month in (1, 4, 7, 10) if line.period == 'year': return month == 1 return False def _period_line_name(self, line, year, month): """Название строки счёта с учётом периодичности.""" if line.period == 'year' and month == 1: return '%s, ежегодное начисление за %sг.' % (line.name, year) if line.period == 'quarter' and month in QUARTER_NAMES: return '%s, ежеквартальное начисление за %s кв. %sг.' % ( line.name, QUARTER_NAMES[month], year, ) return '%s %s-%s' % (line.name, year, month) def _prepare_invoice_vals(self, line, year, month): self.ensure_one() sale_journal = self.env['account.journal'].search([ ('type', '=', 'sale'), ('company_id', '=', self.company_id.id), ], limit=1) origin = 'Начисление по договору аренды за %s %s-%s' % ( line.name, year, month, ) return { 'ref': origin, 'invoice_origin': origin, 'move_type': 'out_invoice', 'journal_id': sale_journal.id or False, 'partner_id': self.commercial_partner_id.id, 'partner_shipping_id': self.partner_id.id, 'fiscal_position_id': self.partner_id.property_account_position_id.id, 'team_id': False, 'invoice_date_due': ( date(year, month, 1) if self.day_inv == 0 else date(year, month, self.day_inv) ), 'invoice_date': ( date(year, month, calendar.monthrange(year, month)[1]) if self.day_upd == 0 else date(year, month, self.day_upd) ), 'month': month, 'year': year, 'company_id': self.company_id.id, 'mt_contract_id': self.contract_id.id if self.contract_id else False, 'comm_contract_id': self.id, 'kladov': self.company_id.chief_id.id if self.company_id.chief_id else False, } def _income_account_id(self, line): return line.product_id.categ_id.property_account_income_categ_id.id or False def create_invoice(self, year=False, month=False): """Создать счета за период по всем строкам договоров self. Возвращает текстовый отчёт об операции (сообщения по строкам). """ result = '' Move = self.env['account.move'] for contract in self: if not year or not month: year = datetime.today().year month = datetime.today().month for line in contract.line_ids: if not contract._is_period_due(line, month): continue origin = 'Начисление по договору аренды за %s %s-%s' % ( line.name, year, month, ) existing = Move.search([ ('commercial_partner_id', '=', contract.commercial_partner_id.id), ('state', '!=', 'cancel'), ('invoice_origin', '=', origin), ('comm_contract_id', '=', contract.id), ], limit=1) if existing: result += ( 'Счёт за указанный период за данную услугу уже есть в базе. ' 'Например, %s. Пожалуйста, отмените старый счёт, прежде чем ' 'создавать новый.' % existing.name ) continue invoice_vals = contract._prepare_invoice_vals(line, year, month) account_id = contract._income_account_id(line) if line.counter_id: line_vals, message = contract._prepare_counter_line( line, year, month, account_id, ) result += message if not line_vals: continue invoice_vals['invoice_line_ids'] = line_vals else: invoice_vals['invoice_line_ids'] = contract._prepare_flat_line( line, year, month, account_id, ) result += ( 'Создано начисление за период: %s, услуга %s, цена %s; ' % (line.period, line.product_id.name, line.price) ) new_inv = Move.create(invoice_vals) new_inv.set_ons() return result def _prepare_counter_line(self, line, year, month, account_id): """Строка счёта по показаниям счётчика (расход за период).""" CounterVal = self.env['comm.counterval'] current = CounterVal.search([ ('counter_id', '=', line.counter_id.id), ('year', '=', year), ('month', '=', month), ], limit=1) previous = CounterVal.search([ ('counter_id', '=', line.counter_id.id), ('id', '!=', current.id if current else 0), ], order='year desc, month desc', limit=1) if not (current and previous) or (current.name - previous.name) <= 0: return [], ( 'для услуги %s не удалось вычислить потребление; ' % line.product_id.name ) consumption = current.name - previous.name message = '%s, цена %s потребление: %s' % ( line.product_id.name, line.price, consumption, ) vals = { 'product_id': line.product_id.id, 'product_uom_id': line.counter_id.type.uom.id, 'name': self._period_line_name(line, year, month), 'quantity': consumption, 'tax_ids': [(6, 0, [line.tax_id.id])] if line.tax_id else False, 'price_unit': line.price, 'partner_id': self.partner_id.parent_id.id or self.partner_id.id, } # account_id задаём только если найден счёт дохода — иначе Odoo # подставит счёт по умолчанию (из журнала/категории продукта). if account_id: vals['account_id'] = account_id return [(0, 0, vals)], message def _prepare_flat_line(self, line, year, month, account_id): """Строка счёта без счётчика (фиксированная сумма за период).""" if line.period == 'year' and month == 1: name = '%s, ежегодное начисление за %sг.' % (line.name, year) elif line.period == 'quarter' and month in QUARTER_NAMES: name = '%s, ежеквартальное начисление за %s кв. %sг.' % ( line.name, QUARTER_NAMES[month], year, ) else: last_day = calendar.monthrange(year, month)[1] name = '%s %s , период 01.%s.%s-%s.%s.%s' % ( line.product_id.name, self.building_id.name, month, year, last_day, month, year, ) vals = { 'product_id': line.product_id.id, 'name': name, 'quantity': 1, 'price_unit': line.price, 'tax_ids': [(6, 0, [line.tax_id.id])] if line.tax_id else False, 'product_uom_id': line.product_id.uom_id.id, } if account_id: vals['account_id'] = account_id return [(0, 0, vals)] # ------------------------------------------------------------------ # Действия # ------------------------------------------------------------------ def action_view_invoice(self): self.ensure_one() action = self.env['ir.actions.act_window']._for_xml_id( 'account.action_move_out_invoice_type' ) action.update({ 'context': dict( self.env.context, default_partner_id=self.partner_id.id, group_by=False, ), 'domain': [ ('commercial_partner_id', '=', self.commercial_partner_id.id), ('comm_contract_id', '=', self.id), ], }) return action def create_fincontract(self): """Создать связанный российский договор (partner.contract.customer).""" Contract = self.env['partner.contract.customer'] profile = self.env['contract.profile'].search([], limit=1) for rec in self: partner_type = 'company' if rec.partner_id.is_company else 'person' vals = { 'date_start': rec.date_start or fields.Date.today(), 'date_end': rec.date_end or fields.Date.today(), 'type': 'customer', 'partner_id': rec.partner_id.id, 'partner_type': partner_type, 'company_id': rec.company_id.id, } if profile: vals['profile_id'] = profile.id rec.contract_id = Contract.create(vals) @api.constrains('contract_id') def _sync_fincontract(self): for rec in self: if rec.contract_id: rec.contract_id.write({ 'date_start': rec.date_start, 'date_end': rec.date_end, 'partner_id': rec.partner_id.id, 'company_id': rec.company_id.id, })