Skip to content

Commit

Permalink
[UPD] sale_forecast: import wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
ntsirintanis committed Jul 22, 2024
1 parent 470a91a commit 82893b4
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 4 deletions.
1 change: 1 addition & 0 deletions sale_forecast/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"security/sale_security.xml",
"views/sale_forecast_view.xml",
"wizards/sale_forecast_wizard_view.xml",
"wizards/wizard_sale_forecast_import.xml",
],
"license": "LGPL-3",
"installable": True,
Expand Down
1 change: 1 addition & 0 deletions sale_forecast/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ access_sale_forecast_system,sale.forecast.system,model_sale_forecast,sales_team.
access_sale_forecast_sheet,access_sale_forecast_sheet,sale_forecast.model_sale_forecast_sheet,sales_team.group_sale_manager,1,1,1,1
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
access_sale_forecast_wizard,access_sale_forecast_wizard,sale_forecast.model_sale_forecast_wizard,sales_team.group_sale_manager,1,1,1,1
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
11 changes: 7 additions & 4 deletions sale_forecast/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.

Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.

See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
Expand Down Expand Up @@ -274,7 +275,7 @@
margin-left: 2em ;
margin-right: 2em }

pre.code .ln { color: grey; } /* line numbers */
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
Expand All @@ -300,7 +301,7 @@
span.pre {
white-space: pre }

span.problematic {
span.problematic, pre.problematic {
color: red }

span.section-subtitle {
Expand Down Expand Up @@ -416,7 +417,9 @@ <h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
Expand Down
1 change: 1 addition & 0 deletions sale_forecast/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import sale_forecast_sheet
from . import sale_forecast_sheet_line
from . import sale_forecast_wizard
from . import wizard_sale_forecast_import
189 changes: 189 additions & 0 deletions sale_forecast/wizards/wizard_sale_forecast_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# pylint: disable=no-member,protected-access,invalid-name,no-self-use
import base64
import logging
from datetime import datetime

from odoo import _, fields, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__) # pylint: disable=invalid-name


class WizardSaleForecastImport(models.TransientModel):
"""Import sale forecast"""

_name = "wizard.sale.forecast.import"
_description = "Import sale forecast records"

file_import = fields.Binary("Import Forecast")
file_name = fields.Char("file name")

def action_process_import(self):
"""Actually process the uploaded file to import it."""
self.ensure_one()
if not self.file_import:
raise UserError(_("Please attach a file containing product information."))
(
rows,
date_headers,
default_code_index,
date_index,
key_index,
) = self._import_file()
aggregate_info = self._aggregate_info(
rows, date_headers, default_code_index, date_index, key_index
)
self._process_import(aggregate_info)

def _import_file(self):
def get_field_index(header_row, name):
"""Get index of column in input file."""
try:
index = header_row.index(name)
return index
except ValueError as error:
raise UserError(
_("Row header name %s is not found in file") % name
) from error

self.ensure_one()
lst = self._get_rows()
if not lst or not lst[0]:
raise UserError(_("Import file is empty or unreadable"))
rows = lst[1]
header_row = rows[1]
date_headers = header_row[4:]
product_headers = header_row[:4]
(product_category_index, product_index, default_code_index, key_index,) = (
get_field_index(product_headers, name)
for name in [
"Product Category",
"Product",
"Item Code (SKU)",
"Key",
]
)
date_index = []
date_index += [get_field_index(header_row, name) for name in date_headers]
return rows, date_headers, default_code_index, date_index, key_index

def _get_rows(self):
"""Get rows from data_file."""
self.ensure_one()
import_model = self.env["base_import.import"]
data_file = base64.b64decode(self.file_import)
importer = import_model.create({"file": data_file, "file_name": self.file_name})
return importer._read_file({"quoting": '"', "separator": ","})

def _aggregate_info(
self, rows, date_headers, default_code_index, date_index, key_index
):
aggregate_info = dict()
for row in rows[2:]:
location = row[key_index].strip()
if not location:
continue
if location == "Total":
continue
default_code = row[default_code_index].strip()
aggregate_info[default_code] = aggregate_info.get(default_code, [])
for date, index in zip(date_headers, date_index):
quantity = (
float(row[index].replace(",", "").replace(".", ""))
if row[index]
else 0
)
if quantity <= 0:
continue
if not quantity:
continue
date_forecast = self._date_to_object(date.strip())
if not date_forecast:
continue
aggregate_info[default_code].append(
(
location,
date_forecast,
quantity,
)
)
return aggregate_info

def _date_to_object(self, date):
"""No expired dates"""
date_object = datetime.strptime(date, "%b-%y")
if date_object.date() < fields.Date.today():
return False
return date_object

def _process_import(self, rows):
forecast_model = self.env["sale.forecast"]
location_model = self.env["stock.location"]
location_dict = {
"Sales": location_model.browse(25),
"CS consumption": location_model.browse(30),
"AS consumption": location_model.browse(18),
}
for default_code, location_date_quantity in rows.items():
product = self.env["product.product"].search(
[("default_code", "=", default_code)]
)
if not product:
_logger.warning(
"No product with default code %s exists.",
default_code,
)
continue
if not location_date_quantity:
continue
for location, date, quantity in location_date_quantity:
if location not in location_dict.keys():
_logger.warning(
"No location %s exists.",
location,
)
continue
location_id = location_dict.get(location)
if not location_id:
_logger.warning(
"No location %s exists.",
location,
)
continue
date_range_id = self._get_date_range_id(date)
if not date_range_id:
_logger.warning(
"No monthly date range exists for %s.",
date.strftime("%b-%y"),
)
continue
vals = {
"product_id": product.id,
"location_id": location_id.id,
"product_uom_qty": quantity,
"date_range_id": date_range_id.id,
}
existing_forecast = forecast_model.search(
[
("product_id", "=", vals["product_id"]),
("date_range_id", "=", vals["date_range_id"]),
("location_id", "=", vals["location_id"]),
]
)
if existing_forecast:
_logger.warning(
"Forecast for product %s, location %s, date %s exists, updating...",
(product.name, location, date.strftime("%b-%y")),
)
existing_forecast.write(vals)
continue
forecast_model.create(vals)

def _get_date_range_id(self, date):
date_range_domain = [
("date_start", "<=", date),
("date_end", ">", date),
("type_name", "ilike", "Monthly"),
("active", "=", True),
]
return self.env["date.range"].search(date_range_domain)
42 changes: 42 additions & 0 deletions sale_forecast/wizards/wizard_sale_forecast_import.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" ?>
<odoo>

<record id="wizard_import_sale_forecast_form" model="ir.ui.view">
<field name="name">Import sale forecast</field>
<field name="model">wizard.sale.forecast.import</field>
<field name="arch" type="xml">
<form string="Import">
<field name="file_name" invisible="1" />
<group>
<field name="file_import" class="oe_inline" filename="file_name" />
</group>
<footer>
<button
name="action_process_import"
string="Process Import"
type="object"
class="oe_highlight"
attrs="{'invisible': [('file_import', '=', False)]}"
/>
<button string="Cancel" special="cancel" class="btn-secondary" />
</footer>
</form>
</field>
</record>

<record id="sale_forecast_import_action" model="ir.actions.act_window">
<field name="name">Sale Forecast Import</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">wizard.sale.forecast.import</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>

<menuitem
id="sale_forecast_import_menu"
parent="sale_forecast_planning_menu"
action="sale_forecast_import_action"
sequence="50"
/>

</odoo>

0 comments on commit 82893b4

Please sign in to comment.