Skip to content
Closed
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 product_catalog_aeroo_report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# directory
##############################################################################

from . import controllers
from . import report
from . import wizards
from . import models
4 changes: 2 additions & 2 deletions product_catalog_aeroo_report/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
"name": "Product Catalog Aeroo Report",
"version": "19.0.1.0.0",
"category": "Aeroo Reporting",
"category": "Reporting",
"sequence": 14,
"summary": "",
"author": "ADHOC SA",
Expand All @@ -29,7 +29,6 @@
"images": [],
"depends": [
"product_price_taxes_included",
"report_aeroo",
"sale",
"stock",
],
Expand All @@ -38,6 +37,7 @@
"security/ir.model.access.csv",
"views/product_catalog_report_views.xml",
"report/product_catalog_report_data.xml",
"report/product_catalog_report_templates.xml",
],
"demo": [
"demo/product_template_demo.xml",
Expand Down
6 changes: 6 additions & 0 deletions product_catalog_aeroo_report/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
##############################################################################
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################

from . import main
70 changes: 70 additions & 0 deletions product_catalog_aeroo_report/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
##############################################################################
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################

import io

from odoo import _, http
from odoo.http import content_disposition, request


class ProductCatalogXlsxController(http.Controller):
@http.route(
"/product_catalog/<int:catalog_id>/export.xlsx",
type="http",
auth="user",
readonly=True,
)
def export_catalog_xlsx(self, catalog_id, taxes_included="0", use_planned_price="0"):
catalog = request.env["product.product_catalog_report"].browse(catalog_id)
catalog.check_access_rights("read")
catalog = catalog.with_context(
taxes_included=taxes_included == "1",
use_planned_price=use_planned_price == "1",
)
catalog = catalog.prepare_report()

buffer = io.BytesIO()
import xlsxwriter # noqa: PLC0415

workbook = xlsxwriter.Workbook(buffer, {"in_memory": True})
worksheet = workbook.add_worksheet()

headers = [_("EAN"), _("Ref"), _("Name"), _("Category"), _("Real Stock"), _("Virtual Stock")]
headers += [pl.name for pl in catalog.pricelist_ids]
worksheet.write_row(0, 0, headers)
column_widths = [len(h) for h in headers]

for row_idx, product in enumerate(catalog.get_all_products(), start=1):
row = [
product.barcode or "",
product.default_code or "",
catalog.get_description(product),
product.categ_id.display_name or "",
product.qty_available,
product.virtual_available,
]
for pricelist in catalog.pricelist_ids:
row.append(catalog.get_price(product, pricelist))
worksheet.write_row(row_idx, 0, row)
for col_idx, value in enumerate(row):
column_widths[col_idx] = max(column_widths[col_idx], len(str(value)))

for col_idx, width in enumerate(column_widths):
worksheet.set_column(col_idx, col_idx, width)
workbook.close()
content = buffer.getvalue()
buffer.close()

filename = "Product Catalog - %s.xlsx" % catalog.name
return request.make_response(
content,
headers=[
(
"Content-Type",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
),
("Content-Disposition", content_disposition(filename)),
],
)
2 changes: 1 addition & 1 deletion product_catalog_aeroo_report/demo/product_product_demo.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<odoo noupdate="1">
<record id="catalog_demo4" model="product.product_catalog_report">
<field name="name">Odt categ, Product</field>
<field name="name">By categories PDF, Product</field>
<field name="products_order">name desc</field>
<field name="categories_order">name desc</field>
<field name="include_sub_categories" eval="True"/>
Expand Down
6 changes: 3 additions & 3 deletions product_catalog_aeroo_report/demo/product_template_demo.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<odoo noupdate="1">
<record id="catalog_demo1" model="product.product_catalog_report">
<field name="name">Ods, Template, one prod per line</field>
<field name="name">Detailed XLSX, Template, one prod per line</field>
<field name="products_order">name desc</field>
<field name="categories_order">name desc</field>
<field name="include_sub_categories" eval="True"/>
Expand All @@ -11,7 +11,7 @@
</record>

<record id="catalog_demo2" model="product.product_catalog_report">
<field name="name">Odt simple, Template, prod list</field>
<field name="name">Simple PDF, Template, prod list</field>
<field name="products_order">name desc</field>
<field name="categories_order">name desc</field>
<field name="include_sub_categories" eval="True"/>
Expand All @@ -21,7 +21,7 @@
</record>

<record id="catalog_demo3" model="product.product_catalog_report">
<field name="name">Odt categ, Template, variants</field>
<field name="name">By categories PDF, Template, variants</field>
<field name="products_order">name desc</field>
<field name="categories_order">name desc</field>
<field name="include_sub_categories" eval="True"/>
Expand Down
147 changes: 138 additions & 9 deletions product_catalog_aeroo_report/models/product_catalog_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,46 @@
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################
from odoo import fields, models
from odoo import _, fields, models
from odoo.tools import formatLang


class ProductCatalogReport(models.Model):
_name = "product.product_catalog_report"
_description = "Product Catalog Report with Aeroo"
_description = "Product Catalog Report"

name = fields.Char(
required=True,
)
products_order = fields.Char(
"Products Order Sintax",
help="for eg. name desc",
help="Order expression for products (e.g. 'name asc', 'default_code'). Leave empty to use default order.",
required=False,
)
categories_order = fields.Char(
"Categories Order Sintax",
help="for eg. name desc",
help="Order expression for categories (e.g. 'name asc', 'sequence desc'). Leave empty to use default order.",
)
include_sub_categories = fields.Boolean(
"Include Subcategories?",
help="If enabled, products from all subcategories of the selected categories will also be included.",
)
only_with_stock = fields.Boolean(
"Only With Stock Products?",
help="If enabled, only products with available stock will be included in the catalog.",
)
taxes_included = fields.Boolean(
"Taxes Included?",
help="Export prices with taxes included by default? This value will be"
" used as default on print catalog wizard",
help="When enabled, prices will include applicable taxes. This value is used as default when printing.",
)
print_product_uom = fields.Boolean(
"Print Product UOM?",
help="If enabled, the unit of measure is printed next to each product name.",
)
product_type = fields.Selection(
[("product.template", "Product Template"), ("product.product", "Product")],
required=True,
help="Choose whether the catalog is based on product templates (variants grouped) or individual product variants.",
)
prod_display_type = fields.Selection(
[
Expand All @@ -46,27 +50,32 @@ class ProductCatalogReport(models.Model):
("variants", "Variants"),
],
"Product Display Type",
help="Controls how variants appear in the report: one per line lists each variant separately, "
"product list shows a summary, variants shows attributes.",
)
report_id = fields.Many2one(
"ir.actions.report",
string="Report",
domain=[("report_type", "=", "aeroo"), ("model", "=", "product.product_catalog_report")],
context={"default_report_type": "aeroo", "default_model": "product.product"},
domain=[("report_type", "in", ["qweb-pdf", "qweb-html"]), ("model", "=", "product.product_catalog_report")],
context={"default_report_type": "qweb-pdf", "default_model": "product.product_catalog_report"},
required=True,
help="Select the report format to use when printing this catalog (PDF simple, PDF by categories, or XLSX export).",
)
category_ids = fields.Many2many(
"product.category",
"product_catalog_report_categories",
"product_catalog_report_id",
"category_id",
"Product Categories",
help="Filter the catalog to products from these categories. Leave empty to include all categories.",
)
pricelist_ids = fields.Many2many(
"product.pricelist",
"product_catalog_report_pricelists",
"product_catalog_report_id",
"pricelist_id",
"Pricelist",
help="Select the pricelists whose prices will appear as columns in the catalog.",
)

category_type = fields.Selection(
Expand Down Expand Up @@ -99,8 +108,128 @@ def prepare_report(self):
)
return self.with_context(**context)

def get_categories(self):
self.ensure_one()
category_model = "product.public.category" if self.category_type == "public_category" else "product.category"
categories = self.category_ids
if self.include_sub_categories and categories:
categories = self.env[category_model].search([("id", "child_of", categories.ids)])
if not categories:
return self.env[category_model]
return self.env[category_model].search([("id", "in", categories.ids)], order=self.categories_order or "id")

def get_products(self, category_ids):
self.ensure_one()
if hasattr(category_ids, "ids"):
category_ids = category_ids.ids
elif not isinstance(category_ids, list):
category_ids = [category_ids]
order = self.products_order or "id"
if self.category_type == "public_category":
domain = [("public_categ_ids", "in", category_ids)]
else:
domain = [("categ_id", "in", category_ids)]
if self.only_with_stock:
domain.append(("qty_available", ">", 0))
return self.env[self.product_type].search(domain, order=order)

def get_report_products(self, category):
self.ensure_one()
return self.get_products(category)

def get_all_products(self):
self.ensure_one()
categories = self.get_categories()
if not categories:
return self.env[self.product_type]
return self.get_products(categories)

def get_variant_details(self, product):
self.ensure_one()
if self.product_type != "product.template":
return []
if self.prod_display_type == "prod_per_line":
return [
", ".join(variant.product_template_attribute_value_ids.mapped("display_name"))
for variant in product.product_variant_ids
]
if self.prod_display_type == "prod_list" and len(product.product_variant_ids) > 1:
return [
" / ".join(
[
" ".join(variant.product_template_attribute_value_ids.mapped("name"))
for variant in product.product_variant_ids
]
)
]
if self.prod_display_type == "variants":
return [
"%s: %s" % (line.attribute_id.name, ", ".join(line.value_ids.mapped("name")))
for line in product.attribute_line_ids
]
return []

def get_price(self, product, pricelist):
self.ensure_one()
product_obj = self.env[self.product_type].with_context(pricelist=pricelist.id, whole_pack_price=True)
sale_uom = self.env["product.template"].fields_get(["sale_uom_ids"])
if sale_uom and product.sale_uom_ids:
product_obj = product_obj.with_context(uom=product.sale_uom_ids[0].uom_id.id)
price = product_obj.browse([product.id])._get_contextual_price()
taxes_included = self.env.context.get("taxes_included", self.taxes_included)
if taxes_included:
taxes = product.taxes_id.filtered(lambda tax: tax.company_id == self.env.company)
price = taxes.compute_all(
price,
currency=pricelist.currency_id,
quantity=1.0,
product=product,
)["total_included"]
return price

def get_formatted_price(self, product, pricelist):
self.ensure_one()
price = self.get_price(product, pricelist)
return formatLang(self.env, price, currency_obj=pricelist.currency_id)

def get_description(self, product):
self.ensure_one()
sale_uom = self.env["product.template"].fields_get(["sale_uom_ids"])
product = product.with_context(display_default_code=False)
if not self.print_product_uom:
return product.display_name
if sale_uom and product.sale_uom_ids:
main_uom = product.sale_uom_ids[0].uom_id
else:
main_uom = product.uom_id
description = "%s (%s)" % (product.display_name, main_uom.display_name)
if sale_uom and len(product.sale_uom_ids) > 1:
description = _("%s. Also available in %s") % (
description,
", ".join(product.sale_uom_ids.filtered(lambda x: x.uom_id != main_uom).mapped("uom_id.display_name")),
)
return description

def generate_report(self):
"""Print the catalog"""
"""Print the catalog (PDF via QWeb or XLSX via controller)."""
self.ensure_one()
self = self.prepare_report()
xlsx_report = self.env.ref(
"product_catalog_aeroo_report.report_product_catalog_ods",
raise_if_not_found=False,
)
if xlsx_report and self.report_id == xlsx_report:
from urllib.parse import urlencode

params = urlencode(
{
"taxes_included": int(bool(self.env.context.get("taxes_included", self.taxes_included))),
"use_planned_price": int(bool(self.env.context.get("use_planned_price"))),
}
)
return {
"type": "ir.actions.act_url",
"url": "/product_catalog/%d/export.xlsx?%s" % (self.id, params),
"target": "self",
}
return self.report_id.report_action(self)
1 change: 0 additions & 1 deletion product_catalog_aeroo_report/report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################
from . import parser
Loading
Loading