Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UPD] sale_forecast: import wizard #3

Open
wants to merge 3 commits into
base: 16.0-sale_forecast
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ([email protected])
: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/views/sale_forecast_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<field name="product_qty" />
<field name="daily_qty" />
<field name="product_target_uom_qty" />
<field name="write_date" string="Last Updated On" />
</tree>
</field>
</record>
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
195 changes: 195 additions & 0 deletions sale_forecast/wizards/wizard_sale_forecast_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# 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[0]
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"""
try:
date_object = datetime.strptime(date, "%b-%y")
except:
try:
date_object = datetime.strptime(date, "%y-%b")
except:
raise
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>
Loading