Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
106
ruodoo_sale_control/README.rst
Normal file
106
ruodoo_sale_control/README.rst
Normal file
@@ -0,0 +1,106 @@
|
||||
=================================
|
||||
РуОду — Контроль отгрузки заказов
|
||||
=================================
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
|
||||
|badge1| |badge2|
|
||||
|
||||
Расширение продаж для контроля финансовых условий отгрузки и приёма
|
||||
закрывающих документов. Заказ подтверждается к отгрузке только при
|
||||
выполнении условий по балансу клиента; не прошедший проверку заказ
|
||||
переводится в состояние «Ожидание оплаты».
|
||||
|
||||
Порт модуля ``gok_sale`` (Odoo 17) на платформу РуОду (Odoo 19).
|
||||
|
||||
**Содержание**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Возможности
|
||||
===========
|
||||
|
||||
* Контроль отгрузки заказа по выбранному типу:
|
||||
|
||||
* *По подтверждению* — отгрузка разрешена всегда
|
||||
* *По балансу* — долг клиента покрывает сумму заказа
|
||||
* *По балансу + овердрафт* — с учётом лимита кредита
|
||||
|
||||
* Перевод не прошедшего проверку заказа в состояние «Ожидание оплаты»
|
||||
с записью в чаттер
|
||||
* Повторная проверка всех ожидающих заказов одним действием
|
||||
(``validate_all``) — удобно для планировщика
|
||||
* Активность «Допущено до отгрузки» на подтверждённом заказе
|
||||
* Подсветка заказов с ценами ниже ранее согласованных для клиента
|
||||
* Автоматическая генерация артикула (SKU) для новой номенклатуры
|
||||
* Приём закрывающих документов по счетам (почта, ЭДО, оригинал, скан),
|
||||
в том числе загрузка скан-копий клиентом из портала
|
||||
|
||||
Зависимости
|
||||
===========
|
||||
|
||||
* ``sale_stock`` — заказы на продажу и отгрузка
|
||||
* ``account`` — счета (``account.move``)
|
||||
* ``portal`` — загрузка закрывающих документов из портала клиента
|
||||
|
||||
Настройка
|
||||
=========
|
||||
|
||||
#. В заказе на продажу выберите *Тип контроля отгрузки* (по умолчанию
|
||||
«По подтверждению»).
|
||||
#. Для контроля по балансу заполните на контрагенте лимит кредита и
|
||||
отслеживайте долг (поля выведены в карточку партнёра).
|
||||
|
||||
Использование
|
||||
=============
|
||||
|
||||
#. Оформите заказ на продажу и нажмите «Подтвердить».
|
||||
#. Если условия выполнены — заказ подтверждается штатно, создаётся
|
||||
активность «Допущено до отгрузки».
|
||||
#. Если условия не выполнены — заказ переходит в «Ожидание оплаты».
|
||||
После поступления оплаты повторно подтвердите заказ вручную или
|
||||
запустите повторную проверку всех ожидающих заказов.
|
||||
#. Закрывающие документы регистрируются на счёте (поле «Закрывающий
|
||||
документ») либо загружаются клиентом из портала счёта — тогда
|
||||
создаётся запись со скан-копией.
|
||||
|
||||
Отличия от исходного модуля
|
||||
===========================
|
||||
|
||||
При портировании исправлены дефекты оригинала:
|
||||
|
||||
* контроль баланса ведётся в контексте компании заказа
|
||||
(``with_company``) без изменения текущей компании среды;
|
||||
* тип активности задаётся data-записью, а не поиском по названию;
|
||||
* последовательность SKU поставляется модулем;
|
||||
* приём документов из портала вынесен на отдельный маршрут
|
||||
``/my/closing_document`` (только для авторизованных пользователей).
|
||||
|
||||
Тестирование
|
||||
============
|
||||
|
||||
Модуль покрыт тестами ``TransactionCase`` (генерация SKU, состояние
|
||||
«Ожидание оплаты», проверки отгрузки по типам, повторная валидация,
|
||||
подсветка цен, закрывающие документы). Запуск::
|
||||
|
||||
docker compose run --rm odoo odoo -d <db> -i ruodoo_sale_control \
|
||||
--test-enable --test-tags=/ruodoo_sale_control --stop-after-init
|
||||
|
||||
Авторы
|
||||
======
|
||||
|
||||
* MK.Lab, RuOdoo
|
||||
|
||||
Модуль сопровождается MK.Lab, RuOdoo.
|
||||
|
||||
.. image:: https://ruodoo.ru/logo.png
|
||||
:alt: MK.Lab, RuOdoo
|
||||
:target: https://ruodoo.ru
|
||||
|
||||
Модуль является частью решения РуОду.
|
||||
3
ruodoo_sale_control/__init__.py
Normal file
3
ruodoo_sale_control/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
43
ruodoo_sale_control/__manifest__.py
Normal file
43
ruodoo_sale_control/__manifest__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'РуОду — Контроль отгрузки заказов',
|
||||
'version': '19.0.2026.09.27',
|
||||
'summary': 'Контроль финансовых условий отгрузки и приём закрывающих документов',
|
||||
'description': """
|
||||
РуОду — Контроль отгрузки заказов
|
||||
=================================
|
||||
|
||||
Расширение продаж для контроля отгрузки и электронного документооборота:
|
||||
|
||||
* Контроль финансовых условий заказа перед отгрузкой: по балансу,
|
||||
по балансу с овердрафтом или по подтверждению. Заказ, не прошедший
|
||||
проверку, переводится в состояние «Ожидание оплаты».
|
||||
* Подсветка заказов, где цены ниже ранее согласованных для клиента.
|
||||
* Автоматическая генерация артикула (SKU) для новой номенклатуры.
|
||||
* Приём закрывающих документов по счетам (почта, ЭДО, оригинал, скан),
|
||||
в том числе загрузка скан-копий из портала клиента.
|
||||
|
||||
Порт модуля `gok_sale` (Odoo 17) на платформу RuOdoo (Odoo 19).
|
||||
""",
|
||||
'author': 'MK.Lab, RuOdoo',
|
||||
'website': 'https://ruodoo.ru',
|
||||
'category': 'Sales',
|
||||
'depends': [
|
||||
'base',
|
||||
'mail',
|
||||
'sale_stock',
|
||||
'account',
|
||||
'portal',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_sequence_data.xml',
|
||||
'data/mail_activity_data.xml',
|
||||
'views/sale_control_views.xml',
|
||||
'views/portal_templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'maintainers': ['mklab', 'ruodoo'],
|
||||
}
|
||||
2
ruodoo_sale_control/controllers/__init__.py
Normal file
2
ruodoo_sale_control/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import portal
|
||||
35
ruodoo_sale_control/controllers/portal.py
Normal file
35
ruodoo_sale_control/controllers/portal.py
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo.http import request, route
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
|
||||
|
||||
class SaleControlPortal(CustomerPortal):
|
||||
|
||||
@route(['/my/closing_document'], type='http', auth='user', website=True)
|
||||
def submit_closing_document(self, redirect=None, **post):
|
||||
"""Приём закрывающего документа (скан) от клиента из портала."""
|
||||
name = post.get('name')
|
||||
invoice_id = post.get('invoice_id')
|
||||
redirect_url = post.get('url') or '/my/invoices'
|
||||
|
||||
file_data = False
|
||||
for uploaded in request.httprequest.files.getlist('image_1920'):
|
||||
content = uploaded.read()
|
||||
if content:
|
||||
file_data = base64.b64encode(content)
|
||||
|
||||
if name and invoice_id and file_data:
|
||||
request.env['doc.closeddoc'].sudo().create({
|
||||
'name': name,
|
||||
'date_doc': datetime.date.today(),
|
||||
'invoice_id': int(invoice_id),
|
||||
'channel': 'scan',
|
||||
'file_inv': file_data,
|
||||
})
|
||||
return werkzeug.utils.redirect('/my/invoices')
|
||||
return werkzeug.utils.redirect(redirect_url)
|
||||
11
ruodoo_sale_control/data/ir_sequence_data.xml
Normal file
11
ruodoo_sale_control/data/ir_sequence_data.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="seq_product_sku" model="ir.sequence">
|
||||
<field name="name">Артикул номенклатуры (SKU)</field>
|
||||
<field name="code">SKU</field>
|
||||
<field name="prefix">SKU</field>
|
||||
<field name="padding">6</field>
|
||||
<field name="number_increment">1</field>
|
||||
<field name="number_next">1</field>
|
||||
</record>
|
||||
</odoo>
|
||||
9
ruodoo_sale_control/data/mail_activity_data.xml
Normal file
9
ruodoo_sale_control/data/mail_activity_data.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mail_activity_shipment_allowed" model="mail.activity.type">
|
||||
<field name="name">Допущено до отгрузки</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="icon">fa-truck</field>
|
||||
<field name="delay_count">0</field>
|
||||
</record>
|
||||
</odoo>
|
||||
5
ruodoo_sale_control/models/__init__.py
Normal file
5
ruodoo_sale_control/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
from . import sale_order
|
||||
from . import product
|
||||
from . import closed_doc
|
||||
from . import account_move
|
||||
8
ruodoo_sale_control/models/account_move.py
Normal file
8
ruodoo_sale_control/models/account_move.py
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
closed_doc_id = fields.Many2one('doc.closeddoc', string='Закрывающий документ')
|
||||
21
ruodoo_sale_control/models/closed_doc.py
Normal file
21
ruodoo_sale_control/models/closed_doc.py
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ClosedDoc(models.Model):
|
||||
_name = 'doc.closeddoc'
|
||||
_description = 'Закрывающий документ по счёту'
|
||||
|
||||
name = fields.Char(string='Номер', required=True)
|
||||
date_doc = fields.Date(string='Дата получения документа', required=True)
|
||||
invoice_id = fields.Many2one('account.move', string='Инвойс', required=True)
|
||||
channel = fields.Selection(
|
||||
[
|
||||
('post', 'Почта'),
|
||||
('edi', 'ЭДО'),
|
||||
('original', 'Поступил в офис'),
|
||||
('scan', 'Скан-копия'),
|
||||
],
|
||||
string='Канал получения', default='post', required=True,
|
||||
)
|
||||
file_inv = fields.Binary(string='Файл: скан или XML')
|
||||
34
ruodoo_sale_control/models/product.py
Normal file
34
ruodoo_sale_control/models/product.py
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('default_code'):
|
||||
sku = self.env['ir.sequence'].next_by_code('SKU')
|
||||
if sku:
|
||||
vals['default_code'] = sku
|
||||
return super().create(vals_list)
|
||||
|
||||
def set_sku(self):
|
||||
for rec in self:
|
||||
rec.default_code = self.env['ir.sequence'].next_by_code('SKU')
|
||||
|
||||
def get_fob_price(self):
|
||||
"""Цена FOB из спецификации (BoM), иначе прайсовая цена.
|
||||
|
||||
Спецификации доступны только при установленном ``mrp``; без него
|
||||
возвращаем прайсовую цену.
|
||||
"""
|
||||
self.ensure_one()
|
||||
bom_ids = self['bom_ids'] if 'bom_ids' in self._fields else False
|
||||
if bom_ids:
|
||||
fob = bom_ids[0].bom_line_ids.filtered(
|
||||
lambda ln: ln.product_id.name and 'FOB' in ln.product_id.name
|
||||
)
|
||||
return fob[0].product_id.lst_price if fob else self.lst_price
|
||||
return self.lst_price
|
||||
111
ruodoo_sale_control/models/sale_order.py
Normal file
111
ruodoo_sale_control/models/sale_order.py
Normal file
@@ -0,0 +1,111 @@
|
||||
|
||||
from datetime import date
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
price_alarm = fields.Boolean(
|
||||
string='Цены требуют проверки',
|
||||
compute='_compute_price_alarm', store=True,
|
||||
)
|
||||
type_validation = fields.Selection(
|
||||
[
|
||||
('balance', 'По балансу'),
|
||||
('overdraft', 'По балансу + овердрафт'),
|
||||
('normal', 'По подтверждению'),
|
||||
],
|
||||
string='Тип контроля отгрузки', default='normal',
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection_add=[('wait', 'Ожидание оплаты')],
|
||||
ondelete={'wait': 'set default'},
|
||||
)
|
||||
|
||||
@api.depends('partner_id', 'order_line', 'order_line.price_unit', 'state')
|
||||
def _compute_price_alarm(self):
|
||||
SaleLine = self.env['sale.order.line']
|
||||
for order in self:
|
||||
if order.state != 'draft':
|
||||
order.price_alarm = False
|
||||
continue
|
||||
alarm = False
|
||||
partner = order.partner_id.parent_id or order.partner_id
|
||||
for line in order.order_line:
|
||||
if not line.product_id:
|
||||
continue
|
||||
previous_prices = SaleLine.search([
|
||||
('order_id.partner_id', '=', partner.id),
|
||||
('order_id.state', '=', 'sale'),
|
||||
('product_id', '=', line.product_id.id),
|
||||
]).sorted(
|
||||
key=lambda ln: ln.order_id.date_order or fields.Datetime.now(),
|
||||
reverse=True,
|
||||
).mapped('price_unit')
|
||||
# Сравниваем только с последней отгрузкой этому клиенту.
|
||||
if previous_prices and line.price_unit < previous_prices[0]:
|
||||
alarm = True
|
||||
order.price_alarm = alarm
|
||||
|
||||
def _get_shipment_credit(self):
|
||||
"""Долг и лимит кредита партнёра (с учётом головного контрагента)."""
|
||||
self.ensure_one()
|
||||
partner = self.partner_id.parent_id or self.partner_id
|
||||
return partner.credit, partner.credit_limit
|
||||
|
||||
def _is_shipment_allowed(self):
|
||||
"""Проверка финансовых условий отгрузки по выбранному типу контроля."""
|
||||
self.ensure_one()
|
||||
# Расчёт долга ведём в контексте компании заказа (без мутации env).
|
||||
order = self.with_company(self.company_id)
|
||||
credit, credit_limit = order._get_shipment_credit()
|
||||
if self.type_validation == 'normal':
|
||||
return True
|
||||
if self.type_validation == 'balance':
|
||||
return credit * (-1) >= self.amount_total
|
||||
if self.type_validation == 'overdraft':
|
||||
return credit * (-1) + credit_limit >= self.amount_total
|
||||
return False
|
||||
|
||||
def action_confirm(self):
|
||||
confirmable = self.env['sale.order']
|
||||
for order in self:
|
||||
if order._is_shipment_allowed():
|
||||
order._log_shipment_activity()
|
||||
# Заказ в ожидании оплаты вернём в черновик, чтобы штатный
|
||||
# action_confirm принял его к подтверждению.
|
||||
if order.state == 'wait':
|
||||
order.state = 'draft'
|
||||
confirmable |= order
|
||||
elif order.state != 'wait':
|
||||
order.message_post(
|
||||
body='Проверка фин. условий оставила заказ в ожидании оплаты.'
|
||||
)
|
||||
order.state = 'wait'
|
||||
if confirmable:
|
||||
return super(SaleOrder, confirmable).action_confirm()
|
||||
return True
|
||||
|
||||
def _log_shipment_activity(self):
|
||||
self.ensure_one()
|
||||
activity_type = self.env.ref(
|
||||
'ruodoo_sale_control.mail_activity_shipment_allowed',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not activity_type:
|
||||
return
|
||||
self.env['mail.activity'].create({
|
||||
'res_model_id': self.env.ref('sale.model_sale_order').id,
|
||||
'res_id': self.id,
|
||||
'user_id': self.user_id.id,
|
||||
'activity_type_id': activity_type.id,
|
||||
'date_deadline': date.today(),
|
||||
})
|
||||
|
||||
@api.model
|
||||
def validate_all(self):
|
||||
"""Повторно проверить все заказы в ожидании оплаты (cron/действие)."""
|
||||
for order in self.search([('state', '=', 'wait')]):
|
||||
order.action_confirm()
|
||||
2
ruodoo_sale_control/security/ir.model.access.csv
Normal file
2
ruodoo_sale_control/security/ir.model.access.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_doc_closeddoc_user,doc.closeddoc.user,model_doc_closeddoc,base.group_user,1,1,1,1
|
||||
|
2
ruodoo_sale_control/tests/__init__.py
Normal file
2
ruodoo_sale_control/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
from . import test_sale_control
|
||||
112
ruodoo_sale_control/tests/test_sale_control.py
Normal file
112
ruodoo_sale_control/tests/test_sale_control.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleControl(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company = cls.env.company
|
||||
cls.partner = cls.env['res.partner'].create({'name': 'Покупатель ООО'})
|
||||
cls.product = cls.env['product.product'].create({
|
||||
'name': 'Товар А',
|
||||
'type': 'consu',
|
||||
'lst_price': 100.0,
|
||||
})
|
||||
|
||||
def _make_order(self, type_validation='normal', price_unit=100.0, qty=1):
|
||||
return self.env['sale.order'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'type_validation': type_validation,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': qty,
|
||||
'price_unit': price_unit,
|
||||
})],
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def test_sku_generated_on_create(self):
|
||||
product = self.env['product.product'].create({'name': 'Без артикула'})
|
||||
self.assertTrue(product.default_code)
|
||||
self.assertTrue(product.default_code.startswith('SKU'))
|
||||
|
||||
def test_sku_not_overwritten(self):
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Свой код', 'default_code': 'MY-CODE',
|
||||
})
|
||||
self.assertEqual(product.default_code, 'MY-CODE')
|
||||
|
||||
def test_set_sku_action(self):
|
||||
old = self.product.default_code
|
||||
self.product.set_sku()
|
||||
self.assertNotEqual(self.product.default_code, old)
|
||||
self.assertTrue(self.product.default_code.startswith('SKU'))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def test_wait_state_registered(self):
|
||||
selection = dict(self.env['sale.order']._fields['state'].selection)
|
||||
self.assertIn('wait', selection)
|
||||
|
||||
def test_confirm_normal_passes(self):
|
||||
order = self._make_order(type_validation='normal')
|
||||
order.action_confirm()
|
||||
self.assertEqual(order.state, 'sale')
|
||||
|
||||
def test_confirm_balance_insufficient_waits(self):
|
||||
# Нет кредита (баланс 0) — по балансу отгрузка не проходит.
|
||||
order = self._make_order(type_validation='balance', price_unit=500.0)
|
||||
order.action_confirm()
|
||||
self.assertEqual(order.state, 'wait')
|
||||
|
||||
def test_shipment_activity_created(self):
|
||||
order = self._make_order(type_validation='normal')
|
||||
order.action_confirm()
|
||||
activity_type = self.env.ref(
|
||||
'ruodoo_sale_control.mail_activity_shipment_allowed'
|
||||
)
|
||||
activity = self.env['mail.activity'].search([
|
||||
('res_model', '=', 'sale.order'),
|
||||
('res_id', '=', order.id),
|
||||
('activity_type_id', '=', activity_type.id),
|
||||
])
|
||||
self.assertTrue(activity)
|
||||
|
||||
def test_validate_all_reprocesses(self):
|
||||
order = self._make_order(type_validation='balance', price_unit=500.0)
|
||||
order.action_confirm()
|
||||
self.assertEqual(order.state, 'wait')
|
||||
# Меняем на «по подтверждению» и повторно валидируем всё.
|
||||
order.type_validation = 'normal'
|
||||
self.env['sale.order'].validate_all()
|
||||
self.assertEqual(order.state, 'sale')
|
||||
|
||||
def test_price_alarm_detects_lower_price(self):
|
||||
# Первая подтверждённая продажа по 100.
|
||||
first = self._make_order(type_validation='normal', price_unit=100.0)
|
||||
first.action_confirm()
|
||||
# Новый черновик по цене ниже -> тревога.
|
||||
second = self._make_order(type_validation='normal', price_unit=80.0)
|
||||
second._compute_price_alarm()
|
||||
self.assertTrue(second.price_alarm)
|
||||
# Черновик по цене не ниже -> тревоги нет.
|
||||
third = self._make_order(type_validation='normal', price_unit=120.0)
|
||||
third._compute_price_alarm()
|
||||
self.assertFalse(third.price_alarm)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def test_closed_doc_link(self):
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
doc = self.env['doc.closeddoc'].create({
|
||||
'name': 'ЗД-1',
|
||||
'date_doc': '2026-01-15',
|
||||
'invoice_id': invoice.id,
|
||||
'channel': 'scan',
|
||||
})
|
||||
invoice.closed_doc_id = doc
|
||||
self.assertEqual(invoice.closed_doc_id, doc)
|
||||
self.assertEqual(doc.invoice_id, invoice)
|
||||
33
ruodoo_sale_control/views/portal_templates.xml
Normal file
33
ruodoo_sale_control/views/portal_templates.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="portal_invoice_closing_document"
|
||||
name="Загрузка закрывающих документов"
|
||||
inherit_id="account.portal_invoice_page">
|
||||
<xpath expr="//div[hasclass('o_download_pdf')]" position="after">
|
||||
<form action="/my/closing_document" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<li class="list-group-item flex-grow-1">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="closing_name">Номер</label>
|
||||
<input type="hidden" name="invoice_id" t-att-value="invoice.id"/>
|
||||
<input type="hidden" name="url" t-att-value="invoice.get_portal_url()"/>
|
||||
<input type="text" id="closing_name" name="name"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="closing_file">Выбрать документы</label>
|
||||
<input type="file" id="closing_file" name="image_1920"
|
||||
data-show-upload="true" accept="image/*"/>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Загрузить закрывающие документы
|
||||
<span class="fa fa-long-arrow-right"/>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</form>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
94
ruodoo_sale_control/views/sale_control_views.xml
Normal file
94
ruodoo_sale_control/views/sale_control_views.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ============ Заказ на продажу ============ -->
|
||||
<record id="view_order_form_sale_control" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.sale.control</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//field[@name='payment_term_id']" position="after">
|
||||
<field name="type_validation"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//header" position="inside">
|
||||
<field name="price_alarm" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//sheet" position="before">
|
||||
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
|
||||
invisible="not price_alarm">
|
||||
Возможно, в этом заказе <b>цены ниже, чем должны быть!</b>
|
||||
Заказ будет отправлен на согласование.
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ Партнёр: финансовые показатели ============ -->
|
||||
<record id="view_partner_form_sale_control" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.sale.control</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='category_id']" position="after">
|
||||
<field name="credit_limit"/>
|
||||
<field name="credit"/>
|
||||
<field name="debit"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ Счёт: закрывающий документ ============ -->
|
||||
<record id="view_move_form_sale_control" model="ir.ui.view">
|
||||
<field name="name">account.move.form.sale.control</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='journal_id']" position="after">
|
||||
<field name="closed_doc_id" context="{'default_invoice_id': id}"
|
||||
domain="[('invoice_id','=',id)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ============ Закрывающие документы ============ -->
|
||||
<record id="view_closed_doc_list" model="ir.ui.view">
|
||||
<field name="name">doc.closeddoc.list</field>
|
||||
<field name="model">doc.closeddoc</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Закрывающие документы">
|
||||
<field name="name"/>
|
||||
<field name="date_doc"/>
|
||||
<field name="invoice_id"/>
|
||||
<field name="channel"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_closed_doc_form" model="ir.ui.view">
|
||||
<field name="name">doc.closeddoc.form</field>
|
||||
<field name="model">doc.closeddoc</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="date_doc"/>
|
||||
<field name="invoice_id"/>
|
||||
<field name="channel"/>
|
||||
<field name="file_inv" filename="name"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_closed_doc">
|
||||
<field name="name">Закрывающие документы</field>
|
||||
<field name="res_model">doc.closeddoc</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user