Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC

This commit is contained in:
CI Publish Bot
2026-05-31 21:19:21 +00:00
commit aa4214c195
1213 changed files with 183945 additions and 0 deletions

View File

@ -0,0 +1,9 @@
# Российская локализация - УПД в xml-формате
name: l10n_ru_upd_xml
## Описание
Формирует универсальный передаточный документ в формате XML.
###Для печати:
1. Меню Бухгалтерия - Клиенты - Счета (account.move);
2. Кнопка "Печать УПД в xml-формате".

View File

@ -0,0 +1,3 @@
from . import controllers
from . import models
from . import reports

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
{
'name': "Российская локализация - УПД в xml-формате",
'summary': """
Формирует УПД в формате XML, формат 5.01""",
'description': """
Формирует УПД в формате XML.
Для печати:
1. Меню Бухгалтерия - Клиенты - Счета (account.move);
2. Кнопка "Печать УПД в xml-формате".
""",
'author': "MK.Lab",
'website': "https://www.inf-centre.ru/",
'category': 'Uncategorized',
'version': '19.0.2025.12.11',
"depends": ["web", "base", "account", "account_payment", "l10n_ru_doc", "l10n_ru_base", "uom"],
"data": [
"views/ir_actions_report_view.xml",
"views/res_partner_view.xml",
"views/res_company_view.xml",
"views/res_users_view.xml",
"views/views_uom_okei.xml",
"views/view_account_move.xml",
"reports/report.xml",
"reports/upd_report.xml",
],
"assets": {
"web.assets_backend": [
"l10n_ru_upd_xml/static/src/js/report/action_manager_report.js",
],
},
"external_dependencies": {
"python": [
"lxml"
]
},
"demo": [
"demo/demo.xml",
],
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import main

View File

@ -0,0 +1,91 @@
# Copyright (C) 2014-2015 Grupo ESOC <www.grupoesoc.es>
# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html).
import json
import logging
from werkzeug.urls import url_parse
from odoo.http import content_disposition, request, route, serialize_exception
from odoo.tools import html_escape
from odoo.tools.safe_eval import safe_eval, time
from odoo.addons.web.controllers import report
_logger = logging.getLogger(__name__)
class ReportController(report.ReportController):
@route()
def report_routes(
self, reportname, docids=None, converter=None, options=None, **kwargs
):
if converter != "xml":
return super().report_routes(
reportname,
docids=docids,
converter=converter,
options=options,
**kwargs,
)
if docids:
docids = [int(_id) for _id in docids.split(",")]
data = {**json.loads(options or "{}"), **kwargs}
context = dict(request.env.context)
if "context" in data:
data["context"] = json.loads(data["context"] or "{}")
# Ignore 'lang' here, because the context in data is the one from the
# webclient *but* if the user explicitely wants to change the lang, this
# mechanism overwrites it.
if "lang" in data["context"]:
del data["context"]["lang"]
context.update(data["context"])
report_Obj = request.env["ir.actions.report"]
xml = report_Obj.with_context(**context)._render_qweb_xml(
reportname, docids, data=data
)[0]
xmlhttpheaders = [("Content-Type", "text/xml"), ("Content-Length", len(xml))]
return request.make_response(xml, headers=xmlhttpheaders)
@route()
def report_download(self, data, context=None, token=None):
requestcontent = json.loads(data)
url, report_type = requestcontent[0], requestcontent[1]
reportname = "???"
if report_type != "qweb-xml":
return super().report_download(data, context=context, token=token)
try:
reportname = url.split("/report/xml/")[1].split("?")[0]
docids = None
if "/" in reportname:
reportname, docids = reportname.split("/")
report = request.env["ir.actions.report"]._get_report_from_name(reportname)
filename = None
if docids:
response = self.report_routes(
reportname, docids=docids, converter="xml", context=context
)
ids = [int(x) for x in docids.split(",")]
obj = request.env[report.model].browse(ids)
if report.print_report_name and not len(obj) > 1:
report_name = safe_eval(
report.print_report_name, {"object": obj, "time": time}
)
filename = f"{report_name}.{report.xml_extension}"
else:
data = url_parse(url).decode_query(cls=dict)
if "context" in data:
context = json.loads(context or "{}")
data_context = json.loads(data.pop("context"))
context = json.dumps({**context, **data_context})
response = self.report_routes(
reportname, converter="xml", context=context, **data
)
filename = filename or f"{report.name}.{report.xml_extension}"
response.headers.add("Content-Disposition", content_disposition(filename))
return response
except Exception as e:
_logger.exception(f"Error while generating report {reportname}")
se = serialize_exception(e)
error = {"code": 200, "message": "Odoo Server Error", "data": se}
return request.make_response(html_escape(json.dumps(error)))

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Добавляем код ОКЕИ 796 (штука) к стандартной единице измерения -->
<record id="uom.product_uom_unit" model="uom.uom">
<field name="okei">796</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,7 @@
from . import ir_actions_report
from . import res_company
from . import res_partner
from . import res_users
from . import uom_uom
from . import account_move
from . import account_move_line

View File

@ -0,0 +1,207 @@
from odoo import api, fields, models, _
import hashlib
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
edi = fields.Char(string=_('ID EDI'), compute='_compute_sh1_edi')
kpp = fields.Char(string=_('КПП'), compute='_compute_get_kpp')
def _compute_get_kpp(self):
for s in self:
pid = self.partner_id.parent_id or self.partner_id
s.kpp = pid.kpp if (s.partner_id == s.partner_shipping_id or not s.partner_shipping_id) else s.partner_shipping_id.kpp
@api.depends('name')
def _compute_sh1_edi(self):
hash_object = hashlib.sha1((self.name).encode('utf-8'))
pid = self.partner_id.parent_id or self.partner_id
self.edi = 'ON_NSCHFDOPPR_2BM-' + str(pid.edi) + '_' + str(self.company_id.edi) + '_' + hash_object.hexdigest()
def print_upd(self):
for s in self:
mes = str(s.check_correct_upd()).strip()
if mes != "":
raise UserError(_(u"Не удалось сформировать УПД. Выявлены следующие ошибки:\n{}".format(mes)))
else:
return self.env.ref('l10n_ru_upd_xml.upd_xml_report').sudo().report_action(s.id) # render_qweb_xml(s.id)
def check_correct_upd(self, manually=True):
for s in self:
mes = ""
company = s.company_id
if s.name == '/':
mes += u"Отсутствует наименование документа. Проверидите документ, чтобы назваие сформировалось автоматически.\n"
if not s.only_service and s.get_delivery_doc_name() == '0':
mes += u"Отсутствуют связанные отгрузки.\n"
if not company:
company = self.env.company
if not company:
mes += u"Не указана компания.\n"
else:
if not company.edi:
mes += u"Не указан идентификатор компании для Diadoc.\n"
if not company.name:
mes += u"Не указано наименование компании.\n"
if not company.okpo:
mes += u"Не указано ОКПО компании.\n"
if not company.inn:
mes += u"Не указан ИНН компании.\n"
else:
if len(company.inn) == 12:
if not company.partner_id.last_name_IP:
mes += u"Не указана фамилия ИП для вашей компании.\n"
if not company.partner_id.first_name_IP:
mes += u"Не указано имя ИП для вашей компании.\n"
if not company.partner_id.middle_name_IP:
mes += u"Не указано отчество ИП для вашей компании.\n"
elif len(company.inn) == 10:
if not company.kpp:
mes += u"Не указан КПП компании.\n"
else:
mes += u"Некорректный ИНН компании.\n"
if not company.city:
mes += u"Не указан город компании.\n"
if not company.street:
mes += u"Не указан адрес компании.\n"
if not company.chief_id:
mes += u"Не указан руководитель компании.\n"
else:
if not company.chief_id.function:
mes += u"Не указана должность руководителя компании.\n"
if not company.chief_id.last_name:
mes += u"Не указана фамилия руководителя компании.\n"
if not company.chief_id.first_name:
mes += u"Не указано имя руководителя компании.\n"
if not company.chief_id.second_name:
mes += u"Не указано отчество руководителя компании.\n"
pid = s.partner_id.parent_id
if not pid:
pid = s.partner_id
if not pid:
mes += u"Не указан контрагент.\n"
else:
if not pid.edi:
mes += u"Не указан идентификатор контрагента для Diadoc.\n"
if not pid.name:
mes += u"Не указано наименование контрагента.\n"
if not pid.okpo:
mes += u"Не указано ОКПО контрагента.\n"
if not pid.inn:
mes += u"Не указан ИНН контрагента.\n"
else:
if len(pid.inn) == 12:
if not pid.last_name_IP:
mes += u"Не указана фамилия ИП для контрагента.\n"
if not pid.first_name_IP:
mes += u"Не указано имя ИП для контрагента.\n"
if not pid.middle_name_IP:
mes += u"Не указано отчество ИП для контрагента.\n"
elif len(pid.inn) == 10:
if not pid.kpp:
mes += u"Не указан КПП контрагента.\n"
else:
mes += u"Некорректный ИНН контрагента.\n"
if not pid.city:
mes += u"Не указан город контрагента.\n"
if not pid.street:
mes += u"Не указан адрес контрагента.\n"
if manually:
if not s.edi:
mes += u"Не указан идентификатор документа для Diadoc.\n"
if not s.name:
mes += u"Не указано наименование документа\n"
if not s.invoice_date:
mes += u"Не указана дата документа\n"
if not s.only_service:
gruzootpr = s.gruzootpr
if not gruzootpr:
gruzootpr = pid
if gruzootpr.parent_id:
gruzootpr = gruzootpr.parent_id
if not gruzootpr:
mes += u"Не указан грузоотправитель.\n"
else:
if not gruzootpr.name:
mes += u"Не указано наименование грузоотправителя.\n"
if not gruzootpr.okpo:
mes += u"Не указано ОКПО грузоотправителя.\n"
if not gruzootpr.inn:
mes += u"Не указан ИНН грузоотправителя.\n"
else:
if len(gruzootpr.inn) == 12:
if not gruzootpr.last_name_IP:
mes += u"Не указана фамилия ИП для грузоотправителя.\n"
if not gruzootpr.first_name_IP:
mes += u"Не указано имя ИП для грузоотправителя.\n"
if not gruzootpr.middle_name_IP:
mes += u"Не указано отчество ИП для грузоотправителя.\n"
elif len(gruzootpr.inn) == 10:
if not gruzootpr.kpp:
mes += u"Не указан КПП грузоотправителя.\n"
else:
mes += u"Некорректный ИНН грузоотправителя.\n"
if not gruzootpr.city:
mes += u"Не указан город грузоотправителя.\n"
if not gruzootpr.street:
mes += u"Не указан адрес грузоотправителя.\n"
gruzopol = s.gruzopol
if not gruzopol:
gruzopol = pid
if gruzopol.parent_id:
gruzopol = gruzopol.parent_id
if not gruzopol:
mes += u"Не указан грузополучатель.\n"
else:
if not gruzopol.name:
mes += u"Не указано наименование грузополучателя.\n"
if not gruzopol.okpo:
mes += u"Не указано ОКПО грузополучателя.\n"
if not gruzopol.inn:
mes += u"Не указан ИНН грузополучателя.\n"
else:
if len(gruzopol.inn) == 12:
if not gruzopol.last_name_IP:
mes += u"Не указана фамилия ИП для грузополучателя.\n"
if not gruzopol.first_name_IP:
mes += u"Не указано имя ИП для грузополучателя.\n"
if not gruzopol.middle_name_IP:
mes += u"Не указано отчество ИП для грузополучателя.\n"
elif len(gruzopol.inn) == 10:
if not gruzopol.kpp:
mes += u"Не указан КПП грузополучателя.\n"
else:
mes += u"Некорректный ИНН грузополучателя.\n"
if not gruzopol.city:
mes += u"Не указан город грузополучателя.\n"
if not gruzopol.street:
mes += u"Не указан адрес грузополучателя.\n"
if s.payment_num:
if not s.payment_date:
mes += u"Не указана дата платежки в УПД.\n"
if not s.invoice_line_ids:
mes += u"Отсутствуют строки заказа.\n"
else:
for line in s.invoice_line_ids:
if not line.price_unit:
mes += u"Не указана цена за единицу для товара {}.\n".format(line.name)
if not line.quantity:
mes += u"Не указано количество для товара {}.\n".format(line.name)
if not line.product_uom_id.okei:
mes += u"Не указан код ОКЕИ для единицы измерения {}.\n".format(line.product_uom_id.name)
if not s.mt_contract_id:
mes += u"Не указан договор.\n"
else:
if not s.mt_contract_id.name:
mes += u"Не указано наименование договора.\n"
if not s.mt_contract_id.date_start:
mes += u"Не указана дата договора.\n"
if not s.kladov:
mes += u"Не указано лицо, ответственное за передачу товаров/услуг.\n"
else:
if not s.kladov.partner_id.function:
mes += u"Не указана должность лица, ответственного за передачу товаров/услуг.\n"
return str(mes)

View File

@ -0,0 +1,7 @@
from odoo import models, fields, api, _
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
uom_okei = fields.Char(string=_('Код ОКЕИ'), related='product_uom_id.okei')

View File

@ -0,0 +1,62 @@
from odoo import fields, models, api
class IrActionsReport(models.Model):
_inherit = "ir.actions.report"
report_type = fields.Selection(
selection_add=[("qweb-xml", "XML")], ondelete={"qweb-xml": "set default"}
)
xsd_schema = fields.Binary(
string="XSD Validation Schema",
attachment=True,
help="File with XSD Schema for checking content of result report. Can be empty "
"if validation is not required.",
)
xml_encoding = fields.Selection(
selection=[
("WINDOWS-1251", "WINDOWS-1251") # will be used as default even if nothing is selected
],
string="XML Encoding",
help=(
"Encoding for XML reports. If nothing is selected, "
"then UTF-8 will be applied."
),
)
xml_declaration = fields.Boolean(
string="XML Declaration",
help=(
"""Add `<?xml encoding="..." version="..."?>` at the start """
"""of final report file."""
),
default=True,
)
xml_extension = fields.Char(
default="xml",
help="Extension for XML Reports, by default is `xml`",
)
@api.model
def _render_qweb_xml(self, report_ref, res_ids, data=None):
"""
Call `generate_report` method of report abstract class
`report.<report technical name>` or of standard class for XML report
rendering - `report.report_xml.abstract`
Args:
* docids(list) - IDs of instances for those report will be generated
* data(dict, None) - variables for report rendering
Returns:
* str - result content of report
* str - type of result content
"""
report = self._get_report(report_ref)
report_model = self.env.get(
f"report.{report.report_name}", self.env["report.upd_xml.abstract"]
)
return report_model.generate_report(
ir_report=report, # will be used to get settings of report
docids=res_ids,
data=data or {},
)

View File

@ -0,0 +1,11 @@
from odoo import fields, models, _
class ResCompany(models.Model):
_inherit = 'res.company'
inn = fields.Char(related='partner_id.inn', readonly=False, string=_('ИНН'))
kpp = fields.Char(related='partner_id.kpp', readonly=False, string=_('КПП'))
okpo = fields.Char(related='partner_id.okpo', readonly=False, string=_('ОКПО'))
edi = fields.Char(string='ID EDI', readonly=False)
chief_id = fields.Many2one('res.users', string=_('Управляющий'))

View File

@ -0,0 +1,36 @@
from odoo import api, fields, models, _
class ResPartner(models.Model):
_inherit = 'res.partner'
inn = fields.Char(_('ИНН'), size=12)
kpp = fields.Char(_('КПП'), size=9)
okpo = fields.Char(_('ОКПО'), size=14)
edi = fields.Char(_('ID EDI'))
house = fields.Char(_('Дом'))
office = fields.Char(_('Квартира, офис'))
fias_id = fields.Char(_('Код ФИАС'))
last_name_IP = fields.Char(_('Фамилия ИП'), compute='_compute_get_fio', readonly=False)
first_name_IP = fields.Char(_('Имя ИП'), compute='_compute_get_fio', readonly=False)
middle_name_IP = fields.Char(_('Отчество ИП'), compute='_compute_get_fio', readonly=False)
@api.depends('name')
def _compute_get_fio(self):
for s in self:
if s.name:
name = s.name
if name.find('ИП ') != -1:
name = name[name.find(' ') + 1:]
s.last_name_IP = name[:name.find(' ')]
name = name[name.find(' ') + 1:]
s.first_name_IP = name[:name.find(' ')]
name = name[name.find(' ') + 1:]
s.middle_name_IP = name
else:
s.last_name_IP = ""
s.first_name_IP = ""
s.middle_name_IP = ""
else:
s.last_name_IP = ""
s.first_name_IP = ""
s.middle_name_IP = ""

View File

@ -0,0 +1,25 @@
from odoo import api, fields, models, _
class ResUsers(models.Model):
_inherit = 'res.users'
last_name = fields.Char(string=_('Фамилия'), compute='_compute_update_name')
first_name = fields.Char(string=_('Имя'), compute='_compute_update_name')
second_name = fields.Char(string=_('Отчество'), compute='_compute_update_name')
@api.depends('name')
def _compute_update_name(self):
for s in self:
s.last_name = ''
s.first_name = ''
s.second_name = ''
if s.name:
s.last_name = s.name
s.first_name = ''
s.second_name = ''
if len(s.name.split(' ')) == 3:
s.last_name, s.first_name, s.second_name = s.name.split(' ')
if len(s.name.split(' ')) == 4:
s.last_name, s.first_name, second_name1, second_name2 = s.name.split(' ')
s.second_name = second_name1 + ' ' + second_name2

View File

@ -0,0 +1,7 @@
from odoo import fields, models, _
class UomUom(models.Model):
_inherit = "uom.uom"
okei = fields.Char(string="Код ОКЕИ")

View File

@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html).
from . import report_report_xml_abstract

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="demo_report_xml_view">
<t t-foreach="docs" t-as="o">
<!-- Multicompany -->
<t t-if="o and 'company_id' in o">
<t t-set="company" t-value="o.company_id"></t>
</t>
<t t-if="not o or not 'company_id' in o">
<t t-set="company" t-value="res_company"></t>
</t>
<t t-set="context" t-value="o._context"></t>
<Файл t-att="{'ИдФайл':o.edi or '','ВерсПрог':'[bi-cons.ru] v.1.6','ВерсФорм':'5.01'}">
<СвУчДокОбор t-att="{'ИдОтпр':company.edi,'ИдПол':o.partner_id.edi}">
</СвУчДокОбор>
<Документ t-att="{'КНД':'1115125','Функция':'СЧФДОП','ПоФактХЖ':'Документ об отгрузке товаров (выполнении работ), передаче имущественных прав (документ об оказании услуг)','НаимДокОпр':'Счет-фактура и документ об отгрузке товаров (выполнении работ), передаче имущественных прав (документ об оказании услуг)','ДатаИнфПр':datetime.datetime.now().strftime('%d.%m.%Y'),'ВремИнфПр':datetime.datetime.now().strftime('%H.%M.%S'),'НаимЭконСубСост':company.name or ''}">
<СвСчФакт t-att="{'НомерСчФ':o.name or '','ДатаСчФ':o.invoice_date.strftime('%d.%m.%Y'),'КодОКВ':'643'}">
<СвПрод t-att="{'ОКПО':company.okpo or '','КраткНазв':company.name or ''}">
<ИдСв>
<СвЮЛУч t-att="{'НаимОрг':company.name or '','ИННЮЛ':company.inn or '','КПП':company.kpp or ''}"/>
</ИдСв>
<Адрес>
<АдрРФ t-att="{'КодРегион':company.state_id.code or '','Город':company.city or ''}"/>
</Адрес>
</СвПрод>
<ГрузОт>
<ОнЖе>он же</ОнЖе>
</ГрузОт>
<ГрузПолуч t-att="{'ОКПО':o.partner_id.okpo or '','КраткНазв':o.partner_id.name or ''}">
<ИдСв>
<СвЮЛУч t-att="{'НаимОрг':o.partner_id.name or '','ИННЮЛ':o.partner_id.inn or '','КПП':o.partner_id.kpp or ''}"/>
</ИдСв>
<Адрес>
<АдрРФ t-att="{'КодРегион':o.partner_id.state_id.code or '','Город':o.partner_id.city or ''}"/>
</Адрес>
</ГрузПолуч>
<СвПокуп t-att="{'ОКПО':o.partner_id.okpo or '','КраткНазв':o.partner_id.name or ''}">
<ИдСв>
<СвЮЛУч t-att="{'НаимОрг':o.partner_id.name or '','ИННЮЛ':o.partner_id.inn or '','КПП':o.partner_id.kpp or ''}"/>
</ИдСв>
<Адрес>
<АдрРФ t-att="{'КодРегион':o.partner_id.state_id.code or '','Город':o.partner_id.city or ''}"/>
</Адрес>
</СвПокуп>
</СвСчФакт>
<ТаблСчФакт>
<t t-set="total_price_tax_subtotal" t-value="sum([line.price_unit*line.quantity for line in o.invoice_line_ids])"/>
<t t-set="total_tax_subtotal" t-value="sum([line.price_unit*line.quantity*(line.tax_ids.amount/100) for line in o.invoice_line_ids])"/>
<t t-foreach="o.invoice_line_ids" t-as="line">
<СведТов t-att="{'НомСтр':line_index+1,'НаимТов':line.name or '','ОКЕИ_Тов':line.product_uom_id.okei or '','КолТов':line.quantity or '','ЦенаТов':(('%.2f')%(line.price_subtotal/line.quantity)).replace('.',',') or '','СтТовБезНДС':(('%.2f')%(line.price_subtotal)).replace('.',',') or '','НалСт':(('%.0f')%(line.tax_ids.amount)).replace('.',',')+'%' or 'без НДС','СтТовУчНал':(('%.2f')%(line.price_subtotal*(1+(line.tax_ids.amount/100)))).replace('.',',') or ''}">
<Акциз>
<БезАкциз>без акциза</БезАкциз>
</Акциз>
<СумНал>
<СумНал><t t-esc="(('%.2f')%(line.price_subtotal*(line.tax_ids.amount/100))).replace('.',',') or ''"/></СумНал>
</СумНал>
</СведТов>
</t>
<ВсегоОпл t-att="{'СтТовБезНДСВсего':(('%.2f')%(total_price_tax_subtotal)).replace('.',',') or '','СтТовУчНалВсего':(('%.2f')%(total_price_tax_subtotal+total_tax_subtotal)).replace('.',',') or ''}">
<СумНалВсего>
<СумНал><t t-esc="(('%.2f')%(total_tax_subtotal)).replace('.',',') or ''"/></СумНал>
</СумНалВсего>
</ВсегоОпл>
</ТаблСчФакт>
<Подписант t-att="{'ОблПолн':'0','Статус':'1','ОснПолн':'Должностные обязанности'}">
<ЮЛ t-att="{'ИННЮЛ':company.inn or '','НаимОрг':company.name or '','Должн':company.chief_id.function or ''}">
<ФИО t-att="{'Фамилия':company.chief_id.last_name or '','Имя':company.chief_id.first_name or '','Отчество':company.chief_id.second_name or ''}"/>
</ЮЛ>
</Подписант>
</Документ>
</Файл>
</t>
</template>
</odoo>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="upd_xml_report" model="ir.actions.report">
<field name="name">УПД xml</field>
<field name="model">account.move</field>
<field name="report_type">qweb-xml</field>
<field name="report_name">l10n_ru_upd_xml.demo_report_xml_view</field>
<field name="report_file">l10n_ru_upd_xml.demo_report_xml_view</field>
<field name="print_report_name">'%s' % (object.edi)</field>
<field name="binding_model_id" ref="account.model_account_move"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@ -0,0 +1,177 @@
# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html).
from xml.dom import minidom
from odoo.exceptions import UserError
from odoo.tools.translate import _
from lxml import etree
from odoo import api, models
class ReportXmlAbstract(models.AbstractModel):
_name = "report.upd_xml.abstract"
_description = "Abstract XML Report"
@api.model
def generate_report(self, ir_report, docids, data=None):
data = data or {}
data.setdefault("report_type", "text")
records = self.env[ir_report.model].browse(docids)
for s in records:
mes = ""
company = s.company_id or self.env.company
# --- базовые проверки ---
if s.name == '/':
mes += "Отсутствует наименование документа. Проведите документ.\n"
if not s.only_service:
get_delivery = getattr(s, "get_delivery_doc_name", lambda: '0')
if get_delivery() == '0':
mes += "Отсутствуют связанные отгрузки.\n"
# --- компания ---
if not company:
mes += "Не указана компания.\n"
else:
if not company.edi:
mes += "Не указан идентификатор компании для Diadoc.\n"
if not company.name:
mes += "Не указано наименование компании.\n"
if not company.okpo:
mes += "Не указано ОКПО компании.\n"
if not company.inn:
mes += "Не указан ИНН компании.\n"
else:
if len(company.inn) == 12:
if not company.partner_id.last_name_IP:
mes += "Не указана фамилия ИП компании.\n"
if not company.partner_id.first_name_IP:
mes += "Не указано имя ИП компании.\n"
if not company.partner_id.middle_name_IP:
mes += "Не указано отчество ИП компании.\n"
elif len(company.inn) == 10:
if not company.kpp:
mes += "Не указан КПП компании.\n"
else:
mes += "Некорректный ИНН компании.\n"
if not company.city:
mes += "Не указан город компании.\n"
if not company.street:
mes += "Не указан адрес компании.\n"
if not company.chief_id:
mes += "Не указан руководитель компании.\n"
else:
if not company.chief_id.function:
mes += "Не указана должность руководителя.\n"
if not company.chief_id.last_name:
mes += "Не указана фамилия руководителя.\n"
if not company.chief_id.first_name:
mes += "Не указано имя руководителя.\n"
if not company.chief_id.second_name:
mes += "Не указано отчество руководителя.\n"
# --- контрагент ---
pid = s.partner_id.parent_id or s.partner_id
if not pid:
mes += "Не указан контрагент.\n"
else:
if not pid.edi:
mes += "Не указан идентификатор контрагента.\n"
if not pid.name:
mes += "Не указано наименование контрагента.\n"
if not pid.okpo:
mes += "Не указано ОКПО контрагента.\n"
if not pid.inn:
mes += "Не указан ИНН контрагента.\n"
else:
if len(pid.inn) == 12:
if not pid.last_name_IP:
mes += "Не указана фамилия ИП контрагента.\n"
if not pid.first_name_IP:
mes += "Не указано имя ИП контрагента.\n"
if not pid.middle_name_IP:
mes += "Не указано отчество ИП контрагента.\n"
elif len(pid.inn) == 10:
if not pid.kpp:
mes += "Не указан КПП контрагента.\n"
else:
mes += "Некорректный ИНН контрагента.\n"
if not pid.city:
mes += "Не указан город контрагента.\n"
if not pid.street:
mes += "Не указан адрес контрагента.\n"
# --- документ ---
if not s.edi:
mes += "Не указан идентификатор документа.\n"
if not s.name:
mes += "Не указано наименование документа.\n"
if not s.invoice_date:
mes += "Не указана дата документа.\n"
# --- строки ---
if not s.invoice_line_ids:
mes += "Отсутствуют строки.\n"
else:
for line in s.invoice_line_ids:
if not line.price_unit:
mes += f"Нет цены: {line.name}\n"
if not line.quantity:
mes += f"Нет количества: {line.name}\n"
if not line.product_uom_id.okei:
mes += f"Нет ОКЕИ: {line.product_uom_id.name}\n"
# --- договор ---
# if not s.mt_contract_id:
# mes += "Не указан договор.\n"
# else:
# if not s.mt_contract_id.name:
# mes += "Нет названия договора.\n"
# if not s.mt_contract_id.date_start:
# mes += "Нет даты договора.\n"
# --- ответственный ---
if not s.kladov:
mes += "Не указано ответственное лицо.\n"
else:
if not s.kladov.partner_id.function:
mes += "Не указана должность ответственного.\n"
# ❗ если есть ошибки → стоп
if mes:
raise UserError(_(
"Не удалось сформировать УПД. Выявлены следующие ошибки:\n%s"
) % mes)
data = ir_report._get_rendering_context(ir_report, docids, data)
result_bin = ir_report._render_template(ir_report.report_name, data)
parsed_result_bin = minidom.parseString(result_bin)
result = parsed_result_bin.toprettyxml(indent=" ")
result = "\n".join(
line for line in result.splitlines() if line and not line.isspace()
).encode("utf8")
content = etree.tostring(
etree.fromstring(result),
encoding=ir_report.xml_encoding or "WINDOWS-1251",
xml_declaration=True,
pretty_print=True,
)
return content, "xml"
@api.model
def _get_report_values(self, docids, data=None):
return data or {}

View File

@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="demo_report_xml_view">
<t t-foreach="docs" t-as="o">
<!-- Multicompany -->
<t t-if="o and o.company_id">
<t t-set="company" t-value="o.company_id"/>
</t>
<t t-elif="not o or not o.company_id">
<t t-set="company" t-value="res_company"/>
</t>
<t t-set="pid" t-value="o and o.partner_id and (o.partner_id.parent_id or o.partner_id) or False"/>
<Файл t-att="{
'ИдФайл': o and o.edi and (o.edi + '_0_0_0_0_0_00') or '',
'ВерсФорм': '5.03',
'ВерсПрог': 'Odoo19'
}">
<Документ t-att="{
'КНД': '1115131',
'Функция': 'СЧФДОП',
'ПоФактХЖ': 'Документ об отгрузке товаров (выполнении работ), передаче имущественных прав (документ об оказании услуг)',
'НаимДокОпр': 'Счет-фактура и документ об отгрузке товаров (выполнении работ), передаче имущественных прав (документ об оказании услуг)',
'ДатаИнфПр': time.strftime('%d.%m.%Y'),
'ВремИнфПр': time.strftime('%H.%M.%S'),
'НаимЭконСубСост': company and company.name or ''
}">
<СвСчФакт t-att="{
'НомерДок': o and (o.invoice_origin or o.name) or '',
'ДатаДок': o and o.invoice_date and o.invoice_date.strftime('%d.%m.%Y') or ''
}">
<СвПрод t-att="{'ОКПО': company and company.okpo or ''}">
<ИдСв>
<t t-if="company and company.inn and len(company.inn) == 10">
<СвЮЛУч t-att="{
'НаимОрг': company.name or '',
'ИННЮЛ': company.inn or '',
'КПП': company.kpp or ''
}"/>
</t>
<t t-elif="company and company.inn and len(company.inn) == 12">
<СвИП t-att="{
'ИННФЛ': company.inn or '',
'ИныеСвед': company.name or ''
}">
<ФИО t-att="{
'Фамилия': company.partner_id and company.partner_id.last_name_IP or '',
'Имя': company.partner_id and company.partner_id.first_name_IP or '',
'Отчество': company.partner_id and company.partner_id.middle_name_IP or ''
}"/>
</СвИП>
</t>
</ИдСв>
<Адрес>
<АдрИнф t-att="{
'КодСтр': '643',
'НаимСтран': 'Российская Федерация',
'АдрТекст': (company.city or '') + ', ' + (company.street or '')
}"/>
</Адрес>
</СвПрод>
<t t-if="o and o.only_service == False">
<ГрузОт>
<ГрузОтпр>
<t t-set="gruzootpr" t-value="o.gruzootpr or company.partner_id"/>
<t t-set="gruzootpr" t-value="gruzootpr and (gruzootpr.parent_id or gruzootpr)"/>
<ИдСв>
<t t-if="gruzootpr and gruzootpr.inn and len(gruzootpr.inn) == 10">
<СвЮЛУч t-att="{
'НаимОрг': gruzootpr.name or '',
'ИННЮЛ': gruzootpr.inn or '',
'КПП': gruzootpr.kpp or ''
}"/>
</t>
<t t-elif="gruzootpr and gruzootpr.inn and len(gruzootpr.inn) == 12">
<СвИП t-att="{
'ИННФЛ': gruzootpr.inn or '',
'ИныеСвед': gruzootpr.name or ''
}">
<ФИО t-att="{
'Фамилия': gruzootpr.last_name_IP or '',
'Имя': gruzootpr.first_name_IP or '',
'Отчество': gruzootpr.middle_name_IP or ''
}"/>
</СвИП>
</t>
</ИдСв>
<Адрес>
<АдрИнф t-att="{
'КодСтр': '643',
'НаимСтран': 'Российская Федерация',
'АдрТекст': (gruzootpr.city or '') + ', ' + (gruzootpr.street or '')
}"/>
</Адрес>
</ГрузОтпр>
</ГрузОт>
<t t-set="gruzopol" t-value="o.gruzopol or pid"/>
<t t-set="gruzopol" t-value="gruzopol and (gruzopol.parent_id or gruzopol)"/>
<ГрузПолуч t-att="{'ОКПО': gruzopol and gruzopol.okpo or ''}">
<ИдСв>
<t t-if="gruzopol and gruzopol.inn and len(gruzopol.inn) == 10">
<СвЮЛУч t-att="{
'НаимОрг': gruzopol.name or '',
'ИННЮЛ': gruzopol.inn or '',
'КПП': gruzopol.kpp or ''
}"/>
</t>
<t t-elif="gruzopol and gruzopol.inn and len(gruzopol.inn) == 12">
<СвИП t-att="{
'ИННФЛ': gruzopol.inn or '',
'ИныеСвед': gruzopol.name or ''
}">
<ФИО t-att="{
'Фамилия': gruzopol.last_name_IP or '',
'Имя': gruzopol.first_name_IP or '',
'Отчество': gruzopol.middle_name_IP or ''
}"/>
</СвИП>
</t>
</ИдСв>
<Адрес>
<АдрИнф t-att="{
'КодСтр': '643',
'НаимСтран': 'Российская Федерация',
'АдрТекст': (gruzopol.city or '') + ', ' + (gruzopol.street or '')
}"/>
</Адрес>
</ГрузПолуч>
</t>
<t t-if="o and o.payment_num and o.payment_num != ''">
<СвПРД t-att="{
'НомерПРД': o.payment_num,
'ДатаПРД': o.payment_date or ''
}"/>
</t>
<t t-if="o and o.only_service == False">
<ДокПодтвОтгрНом
РеквНаимДок="Счет-фактура и документ об отгрузке товаров (выполнении работ), передаче имущественных прав (документ об оказании услуг)"
t-att="{
'РеквНомерДок': o.invoice_origin or o.name or '',
'РеквДатаДок': o.invoice_date and o.invoice_date.strftime('%d.%m.%Y') or ''
}"/>
</t>
<СвПокуп t-att="{'ОКПО': pid and pid.okpo or ''}">
<ИдСв>
<t t-if="pid and pid.inn and len(pid.inn) == 10">
<СвЮЛУч t-att="{
'НаимОрг': pid.name or '',
'ИННЮЛ': pid.inn or '',
'КПП': pid.kpp or ''
}"/>
</t>
<t t-elif="pid and pid.inn and len(pid.inn) == 12">
<СвИП t-att="{
'ИННФЛ': pid.inn or '',
'ИныеСвед': pid.name or ''
}">
<ФИО t-att="{
'Фамилия': pid.last_name_IP or '',
'Имя': pid.first_name_IP or '',
'Отчество': pid.middle_name_IP or ''
}"/>
</СвИП>
</t>
</ИдСв>
<Адрес>
<АдрИнф t-att="{
'КодСтр': '643',
'НаимСтран': 'Российская Федерация',
'АдрТекст': (pid.city or '') + ', ' + (pid.street or '')
}"/>
</Адрес>
</СвПокуп>
<ДенИзм КодОКВ="643" НаимОКВ="Российский рубль" />
</СвСчФакт>
<ТаблСчФакт>
<t t-set="total_price_subtotal" t-value="sum(line.price_subtotal for line in o.invoice_line_ids)"/>
<t t-set="total_price_total" t-value="sum(line.price_total for line in o.invoice_line_ids)"/>
<t t-set="total_tax_amount" t-value="total_price_total - total_price_subtotal"/>
<t t-foreach="o.invoice_line_ids" t-as="line">
<t t-set="tax_rate_raw">
<t t-if="line.tax_ids">
<t t-set="first_tax" t-value="line.tax_ids[0]"/>
<t t-if="first_tax.amount != 0">
<t t-esc="str(int(first_tax.amount)) + '%'"/>
</t>
<t t-else="">
<t t-esc="'без НДС'"/>
</t>
</t>
<t t-else="">
<t t-esc="'без НДС'"/>
</t>
</t>
<t t-set="tax_rate" t-value="tax_rate_raw.replace(' ', '').replace('\n', '').replace('\t', '')"/>
<t t-set="line_tax_amount" t-value="line.price_total - line.price_subtotal"/>
<СведТов t-att="{
'НомСтр': line_index+1,
'НаимТов': line.name or '',
'НаимЕдИзм': line.product_uom_id.name or '',
'КолТов': line.quantity or '',
'ЦенаТов': ('%.2f' % (line.price_subtotal / line.quantity if line.quantity else 0)) if line.quantity else '0.00',
'СтТовБезНДС': ('%.2f' % (line.price_subtotal or 0)),
'НалСт': tax_rate,
'СтТовУчНал': ('%.2f' % (line.price_total or 0))
}">
<ДопСведТов t-att="{
'КодТов': line.product_id and line.product_id.default_code or '',
'ГТИН': '0' + (line.product_id and line.product_id.barcode or '') if line.product_id and line.product_id.barcode else '',
'КодВидТов': line.product_id and line.product_id.hs_code or ''
}">
<t t-if="line.product_id and line.product_id.tracking == 'serial'">
<КрНаимСтрПр>Китай</КрНаимСтрПр>
<НомСредИдентТов>
<t t-foreach="line.sale_line_ids" t-as="line_order">
<t t-foreach="line_order.move_ids" t-as="line_move">
<t t-foreach="line_move.move_line_ids.filtered(lambda km: km.state=='done')" t-as="lot">
<t t-if="lot.lot_id and lot.lot_id.cz_code_id">
<КИЗ><t t-esc="lot.lot_id.cz_code_id.code_without_characters"/></КИЗ>
</t>
</t>
</t>
</t>
</НомСредИдентТов>
</t>
</ДопСведТов>
<Акциз>
<БезАкциз>без акциза</БезАкциз>
</Акциз>
<СумНал>
<t t-if="line_tax_amount != 0">
<СумНал><t t-esc="('%.2f' % line_tax_amount)"/></СумНал>
</t>
<t t-else="">
<БезНДС>без НДС</БезНДС>
</t>
</СумНал>
</СведТов>
</t>
<ВсегоОпл t-att="{
'СтТовБезНДСВсего': ('%.2f' % total_price_subtotal),
'СтТовУчНалВсего': ('%.2f' % total_price_total)
}">
<СумНалВсего>
<t t-if="total_tax_amount != 0">
<СумНал><t t-esc="('%.2f' % total_tax_amount)"/></СумНал>
</t>
<t t-else="">
<БезНДС>без НДС</БезНДС>
</t>
</СумНалВсего>
</ВсегоОпл>
</ТаблСчФакт>
<СвПродПер>
<СвПер t-att="{
'СодОпер': 'Товары или услуги по документу',
'ДатаПер': o.invoice_date and o.invoice_date.strftime('%d.%m.%Y') or ''
}">
<ОснПер t-att="{
'РеквНаимДок': 'Счет-фактура',
'РеквНомерДок': o.invoice_origin or o.name or '',
'РеквДатаДок': o.invoice_date and o.invoice_date.strftime('%d.%m.%Y') or ''
}"/>
<СвЛицПер>
<РабОргПрод t-att="{'Должность': o.kladov and o.kladov.partner_id and o.kladov.partner_id.function or ''}">
<t t-if="o.kladov and o.kladov.partner_id and o.kladov.partner_id.name">
<t t-set="name_parts" t-value="o.kladov.partner_id.name.split()"/>
<t t-if="len(name_parts) == 3">
<ФИО t-att="{
'Фамилия': name_parts[0],
'Имя': name_parts[1],
'Отчество': name_parts[2]
}"/>
</t>
<t t-else="">
<ФИО t-att="{
'Фамилия': name_parts[0] if name_parts else '',
'Имя': name_parts[1] if len(name_parts) > 1 else ''
}"/>
</t>
</t>
</РабОргПрод>
</СвЛицПер>
<Тран t-att="{'СвТран': o.transport or 'Без квитанции'}"/>
</СвПер>
</СвПродПер>
<Подписант t-att="{
'СпосПодтПолном': '1',
'Должн': company.chief_id and company.chief_id.function or ''
}">
<t t-if="company.inn and len(company.inn) == 10">
<t t-if="company.chief_id and company.chief_id.second_name">
<ФИО t-att="{
'Фамилия': company.chief_id.last_name or '',
'Имя': company.chief_id.first_name or '',
'Отчество': company.chief_id.second_name or ''
}"/>
</t>
<t t-else="">
<ФИО t-att="{
'Фамилия': company.chief_id and company.chief_id.last_name or '',
'Имя': company.chief_id and company.chief_id.first_name or ''
}"/>
</t>
</t>
<t t-elif="company.inn and len(company.inn) == 12">
<t t-if="company.partner_id and company.partner_id.middle_name_IP">
<ФИО t-att="{
'Фамилия': company.partner_id.last_name_IP or '',
'Имя': company.partner_id.first_name_IP or '',
'Отчество': company.partner_id.middle_name_IP or ''
}"/>
</t>
<t t-else="">
<ФИО t-att="{
'Фамилия': company.partner_id and company.partner_id.last_name_IP or '',
'Имя': company.partner_id and company.partner_id.first_name_IP or ''
}"/>
</t>
</t>
</Подписант>
</Документ>
</Файл>
</t>
</template>
</odoo>

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_upd_xml_upd_xml,upd_xml.upd_xml,model_upd_xml_upd_xml,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_upd_xml_upd_xml upd_xml.upd_xml model_upd_xml_upd_xml base.group_user 1 1 1 1

View File

@ -0,0 +1,56 @@
/** @odoo-module **/
import {download} from "@web/core/network/download";
import {registry} from "@web/core/registry";
function getReportUrl(action) {
let url = `/report/xml/${action.report_name}`;
const actionContext = action.context || {};
if (action.data && JSON.stringify(action.data) !== "{}") {
const encodedOptions = encodeURIComponent(
JSON.stringify(action.data)
);
const encodedContext = encodeURIComponent(
JSON.stringify(actionContext)
);
return `${url}?options=${encodedOptions}&context=${encodedContext}`;
}
if (actionContext.active_ids) {
url += `/${actionContext.active_ids.join(",")}`;
}
return `${url}?context=${encodeURIComponent(JSON.stringify(actionContext))}`;
}
async function triggerDownload(action) {
const data = JSON.stringify([
getReportUrl(action),
action.report_type,
]);
const context = JSON.stringify(action.context || {});
await download({
url: "/report/download",
data: { data, context },
});
}
registry
.category("ir.actions.report handlers")
.add("xml_handler", async function (action, options) {
if (action.report_type !== "qweb-xml") {
return false;
}
await triggerDownload(action);
return true;
});

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import test_upd_xml

View File

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
"""
Tests for l10n_ru_upd_xml — УПД XML generation.
Validates: Requirements 12.2, 12.4
"""
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
from odoo.exceptions import UserError
def _make_company_partner(env):
"""Return or create a company partner with INN (10 digits) and KPP."""
partner = env['res.partner'].search(
[('is_company', '=', True), ('inn', '!=', False), ('kpp', '!=', False)],
limit=1,
)
if not partner:
partner = env['res.partner'].create({
'name': 'ООО Тест УПД',
'is_company': True,
'inn': '7700000001',
'kpp': '770001001',
'okpo': '12345678',
'city': 'Москва',
'street': 'ул. Тестовая, 1',
})
return partner
def _make_out_invoice(env):
"""Create a minimal out_invoice with all fields required for УПД."""
partner = _make_company_partner(env)
journal = env['account.journal'].search(
[('type', '=', 'sale'), ('company_id', '=', env.company.id)], limit=1
)
if not journal:
journal = env['account.journal'].create({
'name': 'Test Sales Journal UPD',
'type': 'sale',
'code': 'TUPD',
})
move = env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner.id,
'journal_id': journal.id,
'invoice_date': '2024-06-01',
})
return move
@tagged('post_install', '-at_install')
class TestUpdXml(TransactionCase):
"""
Tests for УПД XML generation.
Validates: Requirements 12.2, 12.4
"""
def setUp(self):
super().setUp()
self.invoice = _make_out_invoice(self.env)
# ------------------------------------------------------------------
# Requirement 12.2 — XML generation returns a valid XML document
# ------------------------------------------------------------------
def test_upd_xml_render_returns_valid_xml(self):
"""
Req 12.2 — rendering the УПД XML QWeb template for an account.move
returns non-empty bytes that parse as valid XML.
"""
import xml.etree.ElementTree as ET
xml_content, content_type = self.env['ir.actions.report']._render_qweb_xml(
'l10n_ru_upd_xml.demo_report_xml_view', [self.invoice.id]
)
self.assertTrue(xml_content, "УПД XML render should return non-empty content")
self.assertIsInstance(xml_content, bytes, "Rendered content should be bytes")
# Must parse without exception — proves it is valid XML
try:
root = ET.fromstring(xml_content)
except ET.ParseError as exc:
self.fail(f"Rendered УПД is not valid XML: {exc}")
self.assertIsNotNone(root, "Parsed XML root element should not be None")
# ------------------------------------------------------------------
# Requirement 12.4 — missing INN/KPP raises UserError
# ------------------------------------------------------------------
def test_check_correct_upd_missing_inn_returns_error_message(self):
"""
Req 12.4 — check_correct_upd on an account.move whose partner has no INN
returns a non-empty error string mentioning the missing field.
"""
partner_no_inn = self.env['res.partner'].create({
'name': 'Партнёр без ИНН',
'is_company': True,
# inn intentionally omitted
})
journal = self.env['account.journal'].search(
[('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1
)
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner_no_inn.id,
'journal_id': journal.id,
'invoice_date': '2024-06-01',
})
error_msg = move.check_correct_upd(manually=False)
self.assertTrue(
error_msg.strip(),
"check_correct_upd should return a non-empty error string when INN is missing",
)
self.assertIn('ИНН', error_msg, "Error message should mention ИНН")
def test_print_upd_missing_inn_raises_user_error(self):
"""
Req 12.4 — print_upd on an account.move whose partner has no INN
raises UserError describing the missing fields.
"""
partner_no_inn = self.env['res.partner'].create({
'name': 'Партнёр без ИНН для print_upd',
'is_company': True,
# inn intentionally omitted
})
journal = self.env['account.journal'].search(
[('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1
)
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner_no_inn.id,
'journal_id': journal.id,
'invoice_date': '2024-06-01',
})
with self.assertRaises(UserError) as ctx:
move.print_upd()
self.assertIn('ИНН', str(ctx.exception),
"UserError message should mention ИНН")
def test_print_upd_missing_kpp_raises_user_error(self):
"""
Req 12.4 — print_upd on an account.move whose partner has INN (10 digits)
but no KPP raises UserError describing the missing КПП.
"""
partner_no_kpp = self.env['res.partner'].create({
'name': 'Партнёр без КПП',
'is_company': True,
'inn': '7700000099',
# kpp intentionally omitted
})
journal = self.env['account.journal'].search(
[('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1
)
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner_no_kpp.id,
'journal_id': journal.id,
'invoice_date': '2024-06-01',
})
with self.assertRaises(UserError) as ctx:
move.print_upd()
self.assertIn('КПП', str(ctx.exception),
"UserError message should mention КПП")

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ir_actions_report_view_form_report_xml" model="ir.ui.view">
<field name="name">ir.actions.report.view.form.report.xml</field>
<field name="model">ir.actions.report</field>
<field name="inherit_id" ref="base.act_report_xml_view" />
<field name="arch" type="xml">
<button name="associated_view" position="attributes">
<attribute name="invisible">report_type not in ('qweb-pdf', 'qweb-html', 'qweb-text', 'qweb-xml')</attribute>
</button>
<xpath expr="//page[@name='advanced']/group" position="after">
<group
name="xml_reports"
string="XML Report Settings"
invisible = 'report_type!="qweb-xml"'
>
<field name="xsd_schema" />
<field name="xml_encoding" />
<field name="xml_declaration" />
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_company_ru_form" model="ir.ui.view">
<field name="name">res.company.ru.form</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="edi"/>
<field name="chief_id"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_ru_form" model="ir.ui.view">
<field name="name">res.partner.ru.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//group[2]" position="inside">
<field name="edi" invisible="is_company==False"/>
<field name="last_name_IP"/>
<field name="first_name_IP"/>
<field name="middle_name_IP"/>
</xpath>
<xpath expr="//field[@name='street']" position="after">
<field name="house" placeholder="Дом..."/>
<field name="office" placeholder="Офис\кв..."/>
<field name="fias_id" placeholder="Код ФИАС..."/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_users_facsimile_form" model="ir.ui.view">
<field name="name">res.users.signature.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<group>
<field name="last_name" invisible="1"/>
<field name="first_name" invisible="1"/>
<field name="second_name" invisible="1"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,19 @@
<odoo>
<data>
<record id="account_move_form_upd_button_header" model="ir.ui.view">
<field name="name">account.move.form.upd.button.header</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//form/header" position="inside">
<button name="print_upd"
type="object"
string="Печать УПД в xml-формате"
class="btn-primary"
invisible="state != 'posted' or move_type not in ('out_invoice','out_refund')"/>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,29 @@
<odoo>
<record id="uom_rep" model="ir.ui.view">
<field name="name">OKEI code for uom</field>
<field name="model">uom.uom</field>
<field name="inherit_id" ref="uom.product_uom_form_view"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='name']" position="after">
<field name="okei" string="Код единицы товара по ОКЕИ"/>
</xpath>
</data>
</field>
</record>
<!-- <record id="uom_categories_rep" model="ir.ui.view">-->
<!-- <field name="name">OKEI code for uom categories</field>-->
<!-- <field name="model">uom.category</field>-->
<!-- <field name="inherit_id" ref="l10n_ru_doc.view_uomiher_tree"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <list>-->
<!-- <xpath expr="//field[@name='kod']" position="after">-->
<!-- <field name="okei" string="Код по ОКЕИ"/>-->
<!-- </xpath>-->
<!-- </list>-->
<!-- </field>-->
<!-- </record>-->
</odoo>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="report_xml.assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/report_xml/static/src/js/report/action_manager_report.js"
/>
</xpath>
</template>
</odoo>