Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,305 @@
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,
})