# -*- coding: utf-8 -*- from odoo import api, fields, models, exceptions, tools, _ import pymorphy2 from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from .crutch_fields_header import IP_CONTACT_HEADER, ENTITY_CONTRACT_HEADER, INDIVIDUAL_CONTRACT_HEADER from odoo.exceptions import UserError import re import base64 from io import BytesIO from docx import Document from docx.shared import Pt, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH class PartnerContractCustomer(models.Model): _name = 'partner.contract.customer' _inherit = ['mail.thread', 'mail.activity.mixin', 'mail.render.mixin'] def get_dateend(self): for s in self: if s.date_start: six_months = fields.Datetime.from_string(s.date_start) + relativedelta(months=+11) else: six_months = datetime.today() + relativedelta(months=+11) return fields.Datetime.to_string(six_months) name = fields.Char(string='Номер') date_start = fields.Date(string='Дата договора', required=True, default=fields.Datetime.now()) partner_id = fields.Many2one('res.partner', string=_('Контрагент'), required=True) sec_partner_id = fields.Many2one('res.partner', string=_('Контрагент как в заказе')) company_id = fields.Many2one('res.company', string=_('Компания'), required=True) name_print = fields.Char(string=_('Имя для печати'), compute='_get_name_print') name_print1 = fields.Char(string=_('Имя для печати, И.П.'), compute='_get_name_printip') date_end = fields.Date(string=_('Дата окончания'), required=True, default=get_dateend) name_dirprint = fields.Char(string=_('Имя нашего директора для печати')) name_dirprint1 = fields.Char(string=_('Имя нашего директора для печати И.П.')) lines = fields.One2many('contract.line', 'contract_id', string=_('Договорные цены')) type = fields.Selection( [('customer', 'С покупателем'), ('supplier', 'С поставщиком'), ('other', 'Прочие расчеты'), ], string=_('Тип договора'), default='customer', required=True) saleorder_id = fields.Many2one('sale.order', string=_('Заказ/Сделка')) stamp = fields.Boolean(string=_('Печать и подпись')) signed = fields.Boolean(string=_('Договор подписан')) state = fields.Selection( [('draft', 'Черновик'), ('progress', 'На согласовании'), ('signed', 'Подписан, действует'), ('closed', 'Истёк'), ], string=_('Статус'), default='draft', group_expand='_expand_states', index=True ) is_template = fields.Boolean(_('Это шаблон')) copy_from = fields.Many2one('partner.contract.customer', string=_('Копировать из этого шаблона')) profile_id = fields.Many2one('contract.profile', string=_('Вид договора'), required=True) credit_limit = fields.Float(string=_('Лимит кредита')) guid_1s = fields.Char(_('Код договора из 1С')) buh_code = fields.Char(_('Код договора из бухгалтерии')) payment_term_id = fields.Many2one('account.payment.term', string=_('Условие оплаты')) manager_id = fields.Many2one('res.users', string=_('Менеджер по продажам')) accountant_id = fields.Many2one('res.users', string=_('Бухгалтер по взаиморасчетам')) time_to_delivery_from = fields.Datetime(_('Время доставки от')) time_to_delivery_to = fields.Datetime(_('Время доставки до')) day_of_delivery = fields.Float(_('Дни доставки')) day_of_otgruzki = fields.Float(_('Дни отгрузки')) channel_id = fields.Many2one('saleorder.channel', string=_('Канал продаж')) team_id = fields.Many2one('crm.team', string=_('Команда продаж')) order_days_ids = fields.Many2many(comodel_name='contract.day', relation='orderdays', string=_('Дни доставки'), column1='contract_id', column2='day_id') shipment_days_ids = fields.Many2many(comodel_name='contract.day', relation='shipmentdays', string=_('Дни отгрузки'), column1='contract_id', column2='day_id') # Доработка хедера договора partner_type = fields.Selection(string=_('Тип контрагента'), selection=[ ('person', 'Физ. лицо'), ('company_ip', 'ИП'), ('company', 'Юр. лицо') ], required=True) contract_header = fields.Html(_('Шапка договора')) @api.onchange('partner_type') def generate_contract_header(self): self.ensure_one() self.render_model = 'partner.contract.customer' if self.partner_type == 'company_ip': self.contract_header = IP_CONTACT_HEADER elif self.partner_type == 'person': self.contract_header = INDIVIDUAL_CONTRACT_HEADER else: self.contract_header = ENTITY_CONTRACT_HEADER # # Рендер Jinja выражение типа {{object.field}} result = self._render_template(self.contract_header, self.render_model, res_ids=[self.id]) result = tools.html_sanitize(result[self.id]) self.contract_header = result @api.onchange('sec_partner_id') def set_pid(self): for s in self: s.partner_id = s.sec_partner_id.parent_id if s.sec_partner_id.parent_id else s.sec_partner_id def _expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] def copy_it(self): for s in self: if s.copy_from: for line in s.copy_from.lines: line.copy({'contract_id': s.id}) def _get_name_print(self): morph = pymorphy2.MorphAnalyzer() for s in self: s.name_print = False director = self.env['res.partner'].search([('parent_id', '=', s.partner_id.id), ('type', '=', 'director')], limit=1) if director: if len(director.name.split(' ')) == 3: lastname_old, firstname_old, middlename_old = director.name.split(' ') if lastname_old: lastname_n = morph.parse(lastname_old)[0] if lastname_n.inflect({'gent'}): lastname_n = lastname_n.inflect({'gent'}).word else: lastname_n = lastname_old else: lastname_n = '' if firstname_old: firstname_n = morph.parse(firstname_old)[0] firstname_n = firstname_n.inflect({'gent'}).word else: firstname_n = '' if middlename_old: middlename_n = morph.parse(middlename_old)[0] middlename_n = middlename_n.inflect({'gent'}).word else: middlename_n = '' name_print = lastname_n + ' ' + firstname_n + ' ' + middlename_n s.name_print = name_print.title() def _get_name_print1(self): morph = pymorphy2.MorphAnalyzer() for s in self: director = s.company_id.chief_id.partner_id if s.company_id.chief_id else False # raise exceptions.UserError(str(director)) s.name_dirprint = False if director: if len(director.name.split(' ')) == 3: lastname_old, firstname_old, middlename_old = director.name.split(' ') if lastname_old: lastname_n = morph.parse(lastname_old)[0] lastname_n = lastname_n.inflect({'gent'}).word else: lastname_n = '' if firstname_old: firstname_n = morph.parse(firstname_old)[0] firstname_n = firstname_n.inflect({'gent'}).word else: firstname_n = '' if middlename_old: middlename_n = morph.parse(middlename_old)[0] middlename_n = middlename_n.inflect({'gent'}).word else: middlename_n = '' name_print = lastname_n + ' ' + firstname_n + ' ' + middlename_n s.name_dirprint = name_print.title() @api.model def create(self, vals_list): if isinstance(vals_list, dict): vals_list = [vals_list] records = super().create(vals_list) for record, vals in zip(records, vals_list): if vals.get('is_template'): continue if vals.get('type') == 'customer': sequence_code = 'partner.contract.customer.sequence' elif vals.get('type') == 'supplier': sequence_code = 'partner.contract.supplier.sequence' else: continue name = self.env['ir.sequence'].next_by_code(sequence_code) record.name = name return records # @api.model def write(self, values): if 'state' in values: if self.state != values['state']: msg = 'Статус: ' + dict(self._fields['state'].selection).get(self.state) + ' -> ' + dict( self._fields['state'].selection).get(values['state']) self.message_post(body=msg) res = super(PartnerContractCustomer, self).write(values) return res def _get_name_print1ip(self): for s in self: s.name_dirprint1 = s.company_id.chief_id.partner_id.name if s.company_id.chief_id else False def _get_name_printip(self): for s in self: s.name_print1 = False director = self.env['res.partner'].search([('parent_id', '=', s.partner_id.id), ('type', '=', 'director')], limit=1) if director: s.name_print1 = director.name def print_supp(self): # self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'}) return self.env['report'].get_action(self, 'mta_base.action_mtacontractsupp_report') def print_contract_cust(self): for s in self: if s.saleorder_id: return s.saleorder_id.print_contract() else: raise exceptions.UserError( 'Вы не можете напечатать договор с Клиентом, потому что нет связи с Заказом. Нужно зайти в Заказ и привязать этот договор.') def contract_action_confirm(self): for s in self: if s.state == 'draft': s.state = 'progress' elif s.state == 'progress': s.state = 'signed' def contract_in_draft(self): for s in self: s.state = 'draft' @api.onchange('name') def set_comp_and_partn(self): for s in self: context = s._context order_id = context.get('sale_order_id') if order_id: sale_order = self.env['sale.order'].browse(order_id) s.company_id = sale_order.company_id s.partner_id = sale_order.partner_id @api.onchange('profile_id') def set_payment(self): for s in self: if s.profile_id.payment_term_id: s.payment_term_id = s.profile_id.payment_term_id # @api.constrains('name') def check_name(self): obj = self.search([('name', '=', self.name), ('id', '!=', self.id), ('state', '!=', 'closed')]) if obj: raise exceptions.ValidationError('Договор с таким номером уже существует') """ @api.constrains('profile_id') def check_profile_id(self): contracts = self.search([('partner_id', '=', self.partner_id.id), ('id', '!=', self.id)]) if contracts: profiles_in_contracts = contracts.profile_id # raise exceptions.ValidationError(profiles_in_contracts.ids) if profiles_in_contracts: ads = self.env['contract.allowed.profiles'].search( [('allowed_profiles', 'in', profiles_in_contracts.ids)]) if ads: raise exceptions.ValidationError((self.profile_id.name, ads.name)) # raise exceptions.ValidationError(contracts) """ def action_report_contract_with_format(self, report_format): if report_format == 'docx': file_data, filename = self._generate_contract_docx() mimetype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' elif report_format == 'md': file_data, filename = self._generate_contract_md() mimetype = 'application/md' else: raise UserError("Неверный формат отчёта.") attachment = self.env['ir.attachment'].create({ 'name': filename, 'datas': base64.b64encode(file_data), 'res_model': self._name, 'res_id': self.id, 'mimetype': mimetype or 'application/octet-stream', }) return { 'type': 'ir.actions.act_url', 'url': '/web/content/%s?download=true' % attachment.id, 'target': 'new', } def _strip_html_tags(self, html): clean = re.compile('<.*?>') return re.sub(clean, '', html) def _generate_contract_docx(self): doc = Document() style = doc.styles['Normal'] font = style.font font.name = 'Times New Roman' font.size = Pt(12) company = self.env.company partner = self.partner_id table_header = doc.add_table(rows=1, cols=2) rc_header = table_header.rows[0].cells run_left = rc_header[0].paragraphs[0].add_run(f"{company.name or ''}") run_left.font.bold = True header_paragraph_right = rc_header[1].paragraphs[0] header_paragraph_right.alignment = WD_ALIGN_PARAGRAPH.RIGHT header_run_right = header_paragraph_right.add_run(f"Договор {self.name or ''} от {self.date_start or ''}") header_run_right.font.bold = True header_paragraph_centre = doc.add_paragraph(f"Договор №{self.name or ''} от {self.date_start or ''}\n на поставку продукции") header_paragraph_centre.alignment = WD_ALIGN_PARAGRAPH.CENTER table_city_date = doc.add_table(rows=1, cols=2) row = table_city_date.rows[0].cells run_city = row[0].paragraphs[0].add_run(company.city or '') run_city.font.bold = True paragraph_date = row[1].paragraphs[0] paragraph_date.alignment = WD_ALIGN_PARAGRAPH.RIGHT run_date = paragraph_date.add_run(self.get_date_text(self.date_start) or '') run_date.font.bold = True text_contract_header = self._strip_html_tags(self.contract_header) doc.add_paragraph(text_contract_header) for line in self.lines.sorted(key=lambda r: r.sequence, reverse=False): doc.add_paragraph(line.name or '') doc.add_paragraph(line.punct or '') doc.add_paragraph("Реквизиты и подписи сторон.").alignment = WD_ALIGN_PARAGRAPH.CENTER table = doc.add_table(rows=7, cols=6) table.style = 'Table Grid' table.cell(0, 0).merge(table.cell(0, 2)).text = "Поставщик:" table.cell(0, 3).merge(table.cell(0, 5)).text = "Покупатель:" table.cell(1, 0).merge(table.cell(1, 2)).text = company.name or '' table.cell(1, 3).merge(table.cell(1, 5)).text = partner.name or '' table.cell(2, 0).merge(table.cell(2, 2)).text = f"ИНН {company.inn or ''}" table.cell(2, 3).merge(table.cell(2, 5)).text = f"ИНН {partner.inn or ''}" table.cell(3, 0).merge(table.cell(3, 1)).text = f"ОГРН {company.partner_id.ogrn or ''}" table.cell(3, 3).merge(table.cell(3, 5)).text = f"ОГРН {partner.ogrn or ''}" table.cell(4, 0).merge(table.cell(4, 2)).text = f"КПП {company.kpp or ''}" table.cell(4, 3).merge(table.cell(4, 5)).text = f"КПП {partner.kpp or ''}" company_address = self.address(company) or '' partner_address = self.address(partner) or '' table.cell(5, 0).merge(table.cell(5, 2)).text = f"Юридический адрес: {company_address}" table.cell(5, 3).merge(table.cell(5, 5)).text = f"Юридический адрес: {partner_address}" table.cell(6, 0).merge(table.cell(6, 2)).text = "Подпись Исполнителя:" table.cell(6, 3).merge(table.cell(6, 5)).text = "Подпись Заказчика:" f = BytesIO() doc.save(f) f.seek(0) return f.read(), 'Договор (DOCX).docx' def _generate_contract_md(self): company = self.env.company partner = self.partner_id lines = [] lines.append(f"| **{company.name or ''}** | **Договор {self.name or ''} от {self.date_start or ''}** |") lines.append("| --- | --- |") lines.append(f"**Договор {self.name or ''} от {self.date_start or ''}**\n") lines.append("**на поставку продукции**\n") lines.append("| Город | Дата |") lines.append("| --- | --- |") lines.append(f"| {company.city or ''} | {self.get_date_text(self.date_start)} |\n") text_contract_header = self._strip_html_tags(self.contract_header or "") lines.append(text_contract_header + "\n") for line in self.lines.sorted(key=lambda r: r.sequence): lines.append(f"**{line.name or ''}**") lines.append((line.punct or '') + "\n") lines.append("**Реквизиты и подписи сторон.**\n") lines.append("| Поставщик | | Покупатель | |") lines.append("| --- | --- | --- | --- |") lines.append(f"| {company.name or ''} | | {partner.name or ''} | |") lines.append(f"| ИНН {company.inn or ''} | | ИНН {partner.inn or ''} | |") lines.append(f"| ОГРН {company.ogrn or ''} | | ОГРН {partner.ogrn or ''} | |") lines.append(f"| КПП {company.kpp or ''} | | КПП {partner.kpp or ''} | |") company_address = self.address(company) or '' partner_address = self.address(partner) or '' lines.append(f"| Юридический адрес: {company_address} | | Юридический адрес: {partner_address} | |") lines.append(f"| Подпись Исполнителя | | Подпись Заказчика | |\n") md_content = "\n".join(lines) return md_content.encode('utf-8'), 'Договор (MD).md' def action_print_by_template(self): return { 'name': 'Печать по шаблону', 'type': 'ir.actions.act_window', 'res_model': 'report.template.wizard', 'view_mode': 'form', 'target': 'new', 'context': {'default_contract_id': self.id}, } class Partner(models.Model): _inherit = 'res.partner' contract_count = fields.Integer(string='Договоры', compute='get_count_contract') pol = fields.Selection(string="Пол", selection=[('m', 'Муж.'), ('j', 'Жен'), ], required=False) type = fields.Selection(selection_add=[('director', 'Директор')]) def get_count_contract(self): contract = self.env['partner.contract.customer'] self.contract_count = contract.search_count([('partner_id', '=', self.id)]) def action_view_contract(self): action = self.env.ref('contract.contract_customer_action').read()[0] action['domain'] = [('partner_id', '=', self.id)] return action class ContractLine(models.Model): _name = 'contract.line' contract_id = fields.Many2one('partner.contract.customer', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False) _order = "sequence desc" # name = fields.Text(string='Название для договора') # price_unit = fields.Float('Цена', default=0.0) # product_uom = fields.Many2one('uom.uom', string='Единица измерения') # product_id = fields.Many2one('product.product', string='Услуга', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict') sequence = fields.Integer('Порядок') name = fields.Char('Номер пункта') punct = fields.Html('Текст пункта') @api.onchange('product_id') def set_name(self): for s in self: s.name = s.product_id.name class AllowedProfiles(models.Model): _name = 'contract.allowed.profiles' name = fields.Char(string='Одновременно включены следующие виды договоров:') allowed_profiles = fields.Many2many('contract.profile', string='Виды договоров', required=True) @api.onchange('allowed_profiles') def set_name(self): for s in self: s.name = '' for profile in s.allowed_profiles: s.name += profile.name + ' + ' if s.name: if s.name[-2] == '+': s.name = s.name[:-2] class ContractProfile(models.Model): _name = 'contract.profile' name = fields.Char(string='Вид договора', required=True) payable_account_id = fields.Many2one('account.account', string='Счет кредиторской задолженности', required=True) receivable_account_id = fields.Many2one('account.account', string='Счет дебиторской задолженности', required=True) max_receivable_id = fields.Float(string='Максимальная деб. задолженность', required=True) payment_term_id = fields.Many2one('account.payment.term', string='Условие оплаты', required=True) journal_id = fields.Many2one('account.journal', string='Журнал', required=True) class ContractDay(models.Model): _name = 'contract.day' name = fields.Char('День')