237 lines
9.6 KiB
Python
237 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
|
||
from odoo import models, fields, api, exceptions
|
||
from datetime import timedelta, datetime
|
||
from collections import defaultdict
|
||
import logging
|
||
|
||
_logger = logging.getLogger(__name__)
|
||
|
||
|
||
class MklabForecast(models.Model):
|
||
_name = 'mklab.forecast'
|
||
|
||
name = fields.Char(string='Название', compute='_compute_name')
|
||
start_date = fields.Date(string='Дата прогноза', required=True)
|
||
forecast_line_ids = fields.One2many(
|
||
comodel_name='mklab.forecastline',
|
||
inverse_name='forecast_id',
|
||
string='Строки',
|
||
)
|
||
partner_ids = fields.Many2many(
|
||
comodel_name='res.partner',
|
||
string='Ограничить контрагентами',
|
||
)
|
||
expected_growth = fields.Float(
|
||
string='Ожидаемый рост продаж при акции, %',
|
||
required=True,
|
||
default=20,
|
||
)
|
||
is_promo = fields.Boolean(string='Запланирована акция', compute='_compute_is_promo')
|
||
is_ready_to_calc = fields.Boolean(string='Отправить на расчет')
|
||
total_amount = fields.Float(string='Итого', compute='_compute_total_amount')
|
||
state = fields.Selection(
|
||
selection=[('draft', 'Черновик'), ('active', 'Утвержден')],
|
||
string='Статус',
|
||
default='draft',
|
||
)
|
||
|
||
def _compute_name(self):
|
||
for s in self:
|
||
s.name = str(s.start_date) if s.start_date else ''
|
||
|
||
def _compute_total_amount(self):
|
||
for s in self:
|
||
s.total_amount = sum(s.forecast_line_ids.mapped(lambda r: r.total_amount))
|
||
|
||
def _compute_is_promo(self):
|
||
for s in self:
|
||
promos = self.env['mklab.marketaction'].search([
|
||
('start_date', '<=', s.start_date),
|
||
('end_date', '>=', s.start_date),
|
||
])
|
||
s.is_promo = bool(promos)
|
||
|
||
@api.constrains('is_ready_to_calc')
|
||
def _reset_calculated(self):
|
||
for s in self:
|
||
s.forecast_line_ids.write({'is_calculated': False})
|
||
|
||
def action_validate(self):
|
||
self.state = 'active'
|
||
|
||
def action_cancel(self):
|
||
self.state = 'draft'
|
||
|
||
def fill_lines(self):
|
||
for s in self:
|
||
if s.forecast_line_ids:
|
||
raise exceptions.UserError(
|
||
'Для автоматического заполнения нужно сначала очистить все строки')
|
||
line_obj = self.env['mklab.forecastline']
|
||
products = self.env['product.product'].search([('is_forecast_ok', '=', True)])
|
||
for prod in products:
|
||
line_obj.create({
|
||
'forecast_id': s.id,
|
||
'product_id': prod.id,
|
||
'price': prod.list_price,
|
||
})
|
||
|
||
def create_mrp(self):
|
||
for s in self:
|
||
lines = s.forecast_line_ids.filtered(
|
||
lambda r: r.forecast_value > 0 and not r.mrp_order_id)
|
||
for line in lines:
|
||
bom = line.bom_id
|
||
if not bom:
|
||
raise exceptions.UserError(
|
||
'Для продукта ' + line.product_id.name + ' не найдена ведомость материалов')
|
||
new_order = self.env['mrp.production'].create({
|
||
'product_id': line.product_id.id,
|
||
'product_qty': line.forecast_value,
|
||
'bom_id': bom.id,
|
||
'product_uom_id': line.product_id.uom_id.id,
|
||
'origin': s.name,
|
||
'company_id': self.env.user.company_id.id,
|
||
'date_start': s.start_date,
|
||
})
|
||
line.mrp_order_id = new_order
|
||
|
||
|
||
class MklabForecastLine(models.Model):
|
||
_name = 'mklab.forecastline'
|
||
|
||
product_id = fields.Many2one(
|
||
comodel_name='product.product',
|
||
string='Товар',
|
||
required=True,
|
||
)
|
||
forecast_value = fields.Float(string='Прогноз/план')
|
||
order_value = fields.Float(string='В заказах факт')
|
||
invoice_value = fields.Float(string='Отгружено факт')
|
||
forecast_id = fields.Many2one(
|
||
comodel_name='mklab.forecast',
|
||
string='Прогноз',
|
||
)
|
||
is_calculated = fields.Boolean(string='Пересчитано автоматом')
|
||
price = fields.Float(string='Цена')
|
||
total_amount = fields.Float(string='Итого', compute='_compute_total_amount')
|
||
mrp_order_id = fields.Many2one(
|
||
comodel_name='mrp.production',
|
||
string='Заказ на производство',
|
||
)
|
||
notes = fields.Text(string='Комментарий')
|
||
bom_id = fields.Many2one(
|
||
comodel_name='mrp.bom',
|
||
string='Ведомость материалов',
|
||
)
|
||
|
||
def _compute_total_amount(self):
|
||
for s in self:
|
||
s.total_amount = s.price * s.forecast_value
|
||
|
||
def calc_bom(self):
|
||
for s in self:
|
||
s.bom_id, s.notes = s.product_id.start_planning(s.forecast_value)
|
||
|
||
def get_fact_values(self):
|
||
for s in self:
|
||
for_start = datetime.combine(s.forecast_id.start_date, datetime.min.time())
|
||
for_end = for_start + timedelta(hours=23)
|
||
|
||
if s.forecast_id.partner_ids:
|
||
orders = self.env['sale.order'].search([
|
||
('date_order', '<=', for_end),
|
||
('date_order', '>=', for_start),
|
||
('partner_id', 'in', s.forecast_id.partner_ids.ids),
|
||
])
|
||
else:
|
||
orders = self.env['sale.order'].search([
|
||
('date_order', '<=', for_end),
|
||
('date_order', '>=', for_start),
|
||
])
|
||
ord_value = 0
|
||
inv_value = 0
|
||
for order in orders:
|
||
for line in order.order_line.filtered(lambda r: r.product_id == s.product_id):
|
||
ord_value += line.product_uom_qty
|
||
for inv in order.invoice_ids:
|
||
for line in inv.invoice_line_ids.filtered(lambda r: r.product_id == s.product_id):
|
||
inv_value += line.quantity
|
||
s.invoice_value = inv_value
|
||
s.order_value = ord_value
|
||
|
||
def get_forecast_values(self):
|
||
for s in self:
|
||
start_date_forforecast = s.forecast_id.start_date - timedelta(days=84)
|
||
weekday = s.forecast_id.start_date.weekday()
|
||
|
||
if s.forecast_id.partner_ids:
|
||
orders = self.env['sale.order'].search([
|
||
('date_order', '<', s.forecast_id.start_date),
|
||
('date_order', '>=', start_date_forforecast),
|
||
('dayofweek', '=', weekday),
|
||
('partner_id', 'in', s.forecast_id.partner_ids.ids),
|
||
])
|
||
else:
|
||
orders = self.env['sale.order'].search([
|
||
('date_order', '<', s.forecast_id.start_date),
|
||
('date_order', '>=', start_date_forforecast),
|
||
('dayofweek', '=', weekday),
|
||
])
|
||
f_value = 0
|
||
promos_forecast = self.env['mklab.marketaction'].search([
|
||
('start_date', '<=', s.forecast_id.start_date),
|
||
('end_date', '>=', s.forecast_id.start_date),
|
||
])
|
||
for order in orders:
|
||
promos = self.env['mklab.marketaction'].search([
|
||
('start_date', '<=', order.date_order),
|
||
('end_date', '>=', order.date_order),
|
||
])
|
||
koeff = 0
|
||
if promos:
|
||
mark_lines = self.env['mklab.productline'].search([
|
||
('action_id', '=', promos[0].id),
|
||
('product_id', '=', s.product_id.id),
|
||
])
|
||
pids = self.env['mklab.partnerline'].search([
|
||
('action_id', '=', promos[0].id),
|
||
('partner_id', '=', order.partner_id.id),
|
||
])
|
||
if mark_lines and (pids or not promos[0].partner_line_ids):
|
||
koeff = mark_lines[0].expected_growth
|
||
|
||
is_promo = False
|
||
if promos_forecast:
|
||
mark_lines_f = self.env['mklab.productline'].search([
|
||
('action_id', '=', promos_forecast[0].id),
|
||
('product_id', '=', s.product_id.id),
|
||
])
|
||
pids_f = self.env['mklab.partnerline'].search([
|
||
('action_id', '=', promos_forecast[0].id),
|
||
('partner_id', '=', order.partner_id.id),
|
||
])
|
||
if mark_lines_f and (pids_f or not promos_forecast[0].partner_line_ids):
|
||
is_promo = True
|
||
|
||
order_qty = 0
|
||
for line in order.order_line.filtered(lambda r: r.product_id == s.product_id):
|
||
line_qty = (line.product_uom_qty * (1 + koeff / 100) * (1 + s.forecast_id.expected_growth / 100)
|
||
if is_promo else line.product_uom_qty * (1 + koeff / 100))
|
||
f_value += line_qty
|
||
order_qty += line_qty
|
||
s.forecast_value = round(f_value / 12, 0)
|
||
|
||
@api.model
|
||
def recalc_all(self):
|
||
"""Cron: пересчитывает по 5 строк за раз."""
|
||
lines = self.env['mklab.forecastline'].search(
|
||
[('forecast_id.is_ready_to_calc', '=', True), ('is_calculated', '=', False)],
|
||
limit=5,
|
||
)
|
||
for line in lines:
|
||
line.get_forecast_values()
|
||
line.get_fact_values()
|
||
line.is_calculated = True
|