Skip to content

Commit 94756b3

Browse files
committed
[UPD] sale_forecast: import wizard
1 parent 470a91a commit 94756b3

File tree

6 files changed

+235
-4
lines changed

6 files changed

+235
-4
lines changed

sale_forecast/__manifest__.py

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"security/sale_security.xml",
2020
"views/sale_forecast_view.xml",
2121
"wizards/sale_forecast_wizard_view.xml",
22+
"wizards/wizard_sale_forecast_import.xml",
2223
],
2324
"license": "LGPL-3",
2425
"installable": True,

sale_forecast/security/ir.model.access.csv

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ access_sale_forecast_system,sale.forecast.system,model_sale_forecast,sales_team.
44
access_sale_forecast_sheet,access_sale_forecast_sheet,sale_forecast.model_sale_forecast_sheet,sales_team.group_sale_manager,1,1,1,1
55
access_sale_forecast_sheet_line,access_sale_forecast_sheet_line,sale_forecast.model_sale_forecast_sheet_line,sales_team.group_sale_manager,1,1,1,1
66
access_sale_forecast_wizard,access_sale_forecast_wizard,sale_forecast.model_sale_forecast_wizard,sales_team.group_sale_manager,1,1,1,1
7+
sale_forecast.access_wizard_sale_forecast_import,access_wizard_sale_forecast_import,sale_forecast.model_wizard_sale_forecast_import,base.group_user,1,1,1,1

sale_forecast/static/description/index.html

+7-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
/*
1010
:Author: David Goodger (goodger@python.org)
11-
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
11+
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
1212
:Copyright: This stylesheet has been placed in the public domain.
1313
1414
Default cascading style sheet for the HTML output of Docutils.
15+
Despite the name, some widely supported CSS2 features are used.
1516
1617
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
1718
customize this style sheet.
@@ -274,7 +275,7 @@
274275
margin-left: 2em ;
275276
margin-right: 2em }
276277

277-
pre.code .ln { color: grey; } /* line numbers */
278+
pre.code .ln { color: gray; } /* line numbers */
278279
pre.code, code { background-color: #eeeeee }
279280
pre.code .comment, code .comment { color: #5C6576 }
280281
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -300,7 +301,7 @@
300301
span.pre {
301302
white-space: pre }
302303

303-
span.problematic {
304+
span.problematic, pre.problematic {
304305
color: red }
305306

306307
span.section-subtitle {
@@ -416,7 +417,9 @@ <h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
416417
<div class="section" id="maintainers">
417418
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
418419
<p>This module is maintained by the OCA.</p>
419-
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
420+
<a class="reference external image-reference" href="https://odoo-community.org">
421+
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
422+
</a>
420423
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
421424
mission is to support the collaborative development of Odoo features and
422425
promote its widespread use.</p>

sale_forecast/wizards/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from . import sale_forecast_sheet
22
from . import sale_forecast_sheet_line
33
from . import sale_forecast_wizard
4+
from . import wizard_sale_forecast_import
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# pylint: disable=no-member,protected-access,invalid-name,no-self-use
2+
import base64
3+
import logging
4+
from datetime import datetime
5+
6+
from odoo import _, fields, models
7+
from odoo.exceptions import UserError
8+
9+
_logger = logging.getLogger(__name__) # pylint: disable=invalid-name
10+
11+
12+
class WizardSaleForecastImport(models.TransientModel):
13+
"""Import sale forecast"""
14+
15+
_name = "wizard.sale.forecast.import"
16+
_description = "Import sale forecast records"
17+
18+
file_import = fields.Binary("Import Forecast")
19+
file_name = fields.Char("file name")
20+
21+
def action_process_import(self):
22+
"""Actually process the uploaded file to import it."""
23+
self.ensure_one()
24+
if not self.file_import:
25+
raise UserError(_("Please attach a file containing product information."))
26+
(
27+
rows,
28+
date_headers,
29+
default_code_index,
30+
date_index,
31+
key_index,
32+
) = self._import_file()
33+
aggregate_info = self._aggregate_info(
34+
rows, date_headers, default_code_index, date_index, key_index
35+
)
36+
self._process_import(aggregate_info)
37+
38+
def _import_file(self):
39+
def get_field_index(header_row, name):
40+
"""Get index of column in input file."""
41+
try:
42+
index = header_row.index(name)
43+
return index
44+
except ValueError as error:
45+
raise UserError(
46+
_("Row header name %s is not found in file") % name
47+
) from error
48+
49+
self.ensure_one()
50+
lst = self._get_rows()
51+
if not lst or not lst[0]:
52+
raise UserError(_("Import file is empty or unreadable"))
53+
rows = lst[1]
54+
header_row = rows[1]
55+
date_headers = header_row[4:]
56+
product_headers = header_row[:4]
57+
(product_category_index, product_index, default_code_index, key_index,) = (
58+
get_field_index(product_headers, name)
59+
for name in [
60+
"Product Category",
61+
"Product",
62+
"Item Code (SKU)",
63+
"Key",
64+
]
65+
)
66+
date_index = []
67+
date_index += [get_field_index(header_row, name) for name in date_headers]
68+
return rows, date_headers, default_code_index, date_index, key_index
69+
70+
def _get_rows(self):
71+
"""Get rows from data_file."""
72+
self.ensure_one()
73+
import_model = self.env["base_import.import"]
74+
data_file = base64.b64decode(self.file_import)
75+
importer = import_model.create({"file": data_file, "file_name": self.file_name})
76+
return importer._read_file({"quoting": '"', "separator": ","})
77+
78+
def _aggregate_info(
79+
self, rows, date_headers, default_code_index, date_index, key_index
80+
):
81+
aggregate_info = dict()
82+
for row in rows[2:]:
83+
if row[key_index].strip() == "Total":
84+
continue
85+
aggregate_info[row[default_code_index]] = []
86+
for date, index in zip(date_headers, date_index):
87+
quantity = (
88+
float(row[index].replace(",", "").replace(".", ""))
89+
if row[index]
90+
else 0
91+
)
92+
if quantity <= 0:
93+
continue
94+
date_forecast = self._date_to_object(date.strip())
95+
if not date_forecast:
96+
continue
97+
aggregate_info[row[default_code_index]].append(
98+
(
99+
row[key_index].strip(),
100+
date_forecast,
101+
quantity,
102+
)
103+
)
104+
return aggregate_info
105+
106+
def _date_to_object(self, date):
107+
"""No expired dates"""
108+
date_object = datetime.strptime(date, "%b-%y")
109+
if date_object.date() < fields.Date.today():
110+
return False
111+
return date_object
112+
113+
def _process_import(self, rows):
114+
forecast_model = self.env["sale.forecast"]
115+
location_model = self.env["stock.location"]
116+
location_dict = {
117+
"Sales": location_model.browse(25),
118+
"CS consumption": location_model.browse(30),
119+
"AS consumption": location_model.browse(18),
120+
}
121+
for default_code, location_date_quantity in rows.items():
122+
product = self.env["product.product"].search(
123+
[("default_code", "=", default_code)]
124+
)
125+
if not product:
126+
_logger.warning(
127+
"No product with default code %s exists.",
128+
default_code,
129+
)
130+
continue
131+
if not location_date_quantity:
132+
continue
133+
for location, date, quantity in location_date_quantity:
134+
if location not in location_dict.keys():
135+
_logger.warning(
136+
"No location %s exists.",
137+
location,
138+
)
139+
continue
140+
location_id = location_dict.get(location)
141+
if not location_id:
142+
_logger.warning(
143+
"No location %s exists.",
144+
location,
145+
)
146+
continue
147+
date_range_id = self._get_date_range_id(date)
148+
if not date_range_id:
149+
_logger.warning(
150+
"No monthly date range exists for %s.",
151+
date.strftime("%b-%y"),
152+
)
153+
continue
154+
vals = {
155+
"product_id": product.id,
156+
"location_id": location_id.id,
157+
"product_uom_qty": quantity,
158+
"date_range_id": date_range_id.id,
159+
}
160+
existing_forecast = forecast_model.search(
161+
[
162+
("product_id", "=", vals["product_id"]),
163+
("date_range_id", "=", vals["date_range_id"]),
164+
("location_id", "=", vals["location_id"]),
165+
]
166+
)
167+
if existing_forecast:
168+
_logger.warning(
169+
"Forecast for product %s, location %s, date %s exists, updating...",
170+
(product.name, location, date.strftime("%b-%y")),
171+
)
172+
existing_forecast.write(vals)
173+
continue
174+
forecast_model.create(vals)
175+
176+
def _get_date_range_id(self, date):
177+
date_range_domain = [
178+
("date_start", "<=", date),
179+
("date_end", ">", date),
180+
("type_name", "ilike", "Monthly"),
181+
("active", "=", True),
182+
]
183+
return self.env["date.range"].search(date_range_domain)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" ?>
2+
<odoo>
3+
4+
<record id="wizard_import_sale_forecast_form" model="ir.ui.view">
5+
<field name="name">Import sale forecast</field>
6+
<field name="model">wizard.sale.forecast.import</field>
7+
<field name="arch" type="xml">
8+
<form string="Import">
9+
<field name="file_name" invisible="1" />
10+
<group>
11+
<field name="file_import" class="oe_inline" filename="file_name" />
12+
</group>
13+
<footer>
14+
<button
15+
name="action_process_import"
16+
string="Process Import"
17+
type="object"
18+
class="oe_highlight"
19+
attrs="{'invisible': [('file_import', '=', False)]}"
20+
/>
21+
<button string="Cancel" special="cancel" class="btn-secondary" />
22+
</footer>
23+
</form>
24+
</field>
25+
</record>
26+
27+
<record id="sale_forecast_import_action" model="ir.actions.act_window">
28+
<field name="name">Sale Forecast Import</field>
29+
<field name="type">ir.actions.act_window</field>
30+
<field name="res_model">wizard.sale.forecast.import</field>
31+
<field name="view_mode">form</field>
32+
<field name="target">new</field>
33+
</record>
34+
35+
<menuitem
36+
id="sale_forecast_import_menu"
37+
parent="sale_forecast_planning_menu"
38+
action="sale_forecast_import_action"
39+
sequence="50"
40+
/>
41+
42+
</odoo>

0 commit comments

Comments
 (0)