summaryrefslogtreecommitdiff
path: root/addons/sale/report
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/sale/report
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/sale/report')
-rw-r--r--addons/sale/report/__init__.py6
-rw-r--r--addons/sale/report/invoice_report.py13
-rw-r--r--addons/sale/report/invoice_report_templates.xml21
-rw-r--r--addons/sale/report/report_all_channels_sales.py87
-rw-r--r--addons/sale/report/report_all_channels_sales_views.xml8
-rw-r--r--addons/sale/report/sale_report.py161
-rw-r--r--addons/sale/report/sale_report.xml27
-rw-r--r--addons/sale/report/sale_report_templates.xml230
-rw-r--r--addons/sale/report/sale_report_views.xml76
9 files changed, 629 insertions, 0 deletions
diff --git a/addons/sale/report/__init__.py b/addons/sale/report/__init__.py
new file mode 100644
index 00000000..90248f47
--- /dev/null
+++ b/addons/sale/report/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import sale_report
+from . import invoice_report
+from . import report_all_channels_sales
diff --git a/addons/sale/report/invoice_report.py b/addons/sale/report/invoice_report.py
new file mode 100644
index 00000000..f41cc8c3
--- /dev/null
+++ b/addons/sale/report/invoice_report.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class AccountInvoiceReport(models.Model):
+ _inherit = 'account.invoice.report'
+
+ team_id = fields.Many2one('crm.team', string='Sales Team')
+
+ def _select(self):
+ return super(AccountInvoiceReport, self)._select() + ", move.team_id as team_id"
diff --git a/addons/sale/report/invoice_report_templates.xml b/addons/sale/report/invoice_report_templates.xml
new file mode 100644
index 00000000..06bcd289
--- /dev/null
+++ b/addons/sale/report/invoice_report_templates.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="report_invoice_document_inherit_sale" inherit_id="account.report_invoice_document">
+ <xpath expr="//address" position="attributes">
+ <attribute name="groups">!sale.group_delivery_invoice_address</attribute>
+ </xpath>
+ <xpath expr="//address" position="before">
+ <t t-if="o.partner_shipping_id and (o.partner_shipping_id != o.partner_id)">
+ <t t-set="information_block">
+ <div groups="sale.group_delivery_invoice_address" name="shipping_address_block">
+ <strong>Shipping Address:</strong>
+ <div t-field="o.partner_shipping_id"
+ t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
+ </div>
+ </t>
+ </t>
+ <div t-field="o.partner_id"
+ t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' groups="sale.group_delivery_invoice_address"/>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/sale/report/report_all_channels_sales.py b/addons/sale/report/report_all_channels_sales.py
new file mode 100644
index 00000000..1aa6e960
--- /dev/null
+++ b/addons/sale/report/report_all_channels_sales.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools
+
+
+class PosSaleReport(models.Model):
+ _name = "report.all.channels.sales"
+ _description = "Sales by Channel (All in One)"
+ _auto = False
+
+ name = fields.Char('Order Reference', readonly=True)
+ partner_id = fields.Many2one('res.partner', 'Partner', readonly=True)
+ product_id = fields.Many2one('product.product', string='Product', readonly=True)
+ product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
+ date_order = fields.Datetime(string='Date Order', readonly=True)
+ user_id = fields.Many2one('res.users', 'Salesperson', readonly=True)
+ categ_id = fields.Many2one('product.category', 'Product Category', readonly=True)
+ company_id = fields.Many2one('res.company', 'Company', readonly=True)
+ price_total = fields.Float('Total', readonly=True)
+ pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True)
+ country_id = fields.Many2one('res.country', 'Partner Country', readonly=True)
+ price_subtotal = fields.Float(string='Price Subtotal', readonly=True)
+ product_qty = fields.Float('Product Quantity', readonly=True)
+ analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
+ team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True)
+
+ def _so(self):
+ so_str = """
+ SELECT sol.id AS id,
+ so.name AS name,
+ so.partner_id AS partner_id,
+ sol.product_id AS product_id,
+ pro.product_tmpl_id AS product_tmpl_id,
+ so.date_order AS date_order,
+ so.user_id AS user_id,
+ pt.categ_id AS categ_id,
+ so.company_id AS company_id,
+ sol.price_total / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_total,
+ so.pricelist_id AS pricelist_id,
+ rp.country_id AS country_id,
+ sol.price_subtotal / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_subtotal,
+ (sol.product_uom_qty / u.factor * u2.factor) as product_qty,
+ so.analytic_account_id AS analytic_account_id,
+ so.team_id AS team_id
+
+ FROM sale_order_line sol
+ JOIN sale_order so ON (sol.order_id = so.id)
+ LEFT JOIN product_product pro ON (sol.product_id = pro.id)
+ JOIN res_partner rp ON (so.partner_id = rp.id)
+ LEFT JOIN product_template pt ON (pro.product_tmpl_id = pt.id)
+ LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id)
+ LEFT JOIN uom_uom u on (u.id=sol.product_uom)
+ LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id)
+ WHERE so.state in ('sale','done')
+ """
+ return so_str
+
+ def _from(self):
+ return """(%s)""" % (self._so())
+
+ def get_main_request(self):
+ request = """
+ CREATE or REPLACE VIEW %s AS
+ SELECT id AS id,
+ name,
+ partner_id,
+ product_id,
+ product_tmpl_id,
+ date_order,
+ user_id,
+ categ_id,
+ company_id,
+ price_total,
+ pricelist_id,
+ analytic_account_id,
+ country_id,
+ team_id,
+ price_subtotal,
+ product_qty
+ FROM %s
+ AS foo""" % (self._table, self._from())
+ return request
+
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute(self.get_main_request())
diff --git a/addons/sale/report/report_all_channels_sales_views.xml b/addons/sale/report/report_all_channels_sales_views.xml
new file mode 100644
index 00000000..e6f3cc06
--- /dev/null
+++ b/addons/sale/report/report_all_channels_sales_views.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="report_all_channels_sales_action" model="ir.actions.act_window">
+ <field name="name">Sales Analysis</field>
+ <field name="res_model">sale.report</field>
+ <field name="view_mode">pivot</field>
+ </record>
+</odoo>
diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py
new file mode 100644
index 00000000..76ec1476
--- /dev/null
+++ b/addons/sale/report/sale_report.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import tools
+from odoo import api, fields, models
+
+
+class SaleReport(models.Model):
+ _name = "sale.report"
+ _description = "Sales Analysis Report"
+ _auto = False
+ _rec_name = 'date'
+ _order = 'date desc'
+
+ @api.model
+ def _get_done_states(self):
+ return ['sale', 'done', 'paid']
+
+ name = fields.Char('Order Reference', readonly=True)
+ date = fields.Datetime('Order Date', readonly=True)
+ product_id = fields.Many2one('product.product', 'Product Variant', readonly=True)
+ product_uom = fields.Many2one('uom.uom', 'Unit of Measure', readonly=True)
+ product_uom_qty = fields.Float('Qty Ordered', readonly=True)
+ qty_delivered = fields.Float('Qty Delivered', readonly=True)
+ qty_to_invoice = fields.Float('Qty To Invoice', readonly=True)
+ qty_invoiced = fields.Float('Qty Invoiced', readonly=True)
+ partner_id = fields.Many2one('res.partner', 'Customer', readonly=True)
+ company_id = fields.Many2one('res.company', 'Company', readonly=True)
+ user_id = fields.Many2one('res.users', 'Salesperson', readonly=True)
+ price_total = fields.Float('Total', readonly=True)
+ price_subtotal = fields.Float('Untaxed Total', readonly=True)
+ untaxed_amount_to_invoice = fields.Float('Untaxed Amount To Invoice', readonly=True)
+ untaxed_amount_invoiced = fields.Float('Untaxed Amount Invoiced', readonly=True)
+ product_tmpl_id = fields.Many2one('product.template', 'Product', readonly=True)
+ categ_id = fields.Many2one('product.category', 'Product Category', readonly=True)
+ nbr = fields.Integer('# of Lines', readonly=True)
+ pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True)
+ analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
+ team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True)
+ country_id = fields.Many2one('res.country', 'Customer Country', readonly=True)
+ industry_id = fields.Many2one('res.partner.industry', 'Customer Industry', readonly=True)
+ commercial_partner_id = fields.Many2one('res.partner', 'Customer Entity', readonly=True)
+ state = fields.Selection([
+ ('draft', 'Draft Quotation'),
+ ('sent', 'Quotation Sent'),
+ ('sale', 'Sales Order'),
+ ('done', 'Sales Done'),
+ ('cancel', 'Cancelled'),
+ ], string='Status', readonly=True)
+ weight = fields.Float('Gross Weight', readonly=True)
+ volume = fields.Float('Volume', readonly=True)
+
+ discount = fields.Float('Discount %', readonly=True)
+ discount_amount = fields.Float('Discount Amount', readonly=True)
+ campaign_id = fields.Many2one('utm.campaign', 'Campaign')
+ medium_id = fields.Many2one('utm.medium', 'Medium')
+ source_id = fields.Many2one('utm.source', 'Source')
+
+ order_id = fields.Many2one('sale.order', 'Order #', readonly=True)
+
+ def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
+ with_ = ("WITH %s" % with_clause) if with_clause else ""
+
+ select_ = """
+ coalesce(min(l.id), -s.id) as id,
+ l.product_id as product_id,
+ t.uom_id as product_uom,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.product_uom_qty / u.factor * u2.factor) ELSE 0 END as product_uom_qty,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.qty_delivered / u.factor * u2.factor) ELSE 0 END as qty_delivered,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.qty_invoiced / u.factor * u2.factor) ELSE 0 END as qty_invoiced,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.qty_to_invoice / u.factor * u2.factor) ELSE 0 END as qty_to_invoice,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.price_total / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as price_total,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.price_subtotal / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as price_subtotal,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.untaxed_amount_to_invoice / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as untaxed_amount_to_invoice,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(l.untaxed_amount_invoiced / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END) ELSE 0 END as untaxed_amount_invoiced,
+ count(*) as nbr,
+ s.name as name,
+ s.date_order as date,
+ s.state as state,
+ s.partner_id as partner_id,
+ s.user_id as user_id,
+ s.company_id as company_id,
+ s.campaign_id as campaign_id,
+ s.medium_id as medium_id,
+ s.source_id as source_id,
+ extract(epoch from avg(date_trunc('day',s.date_order)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay,
+ t.categ_id as categ_id,
+ s.pricelist_id as pricelist_id,
+ s.analytic_account_id as analytic_account_id,
+ s.team_id as team_id,
+ p.product_tmpl_id,
+ partner.country_id as country_id,
+ partner.industry_id as industry_id,
+ partner.commercial_partner_id as commercial_partner_id,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(p.weight * l.product_uom_qty / u.factor * u2.factor) ELSE 0 END as weight,
+ CASE WHEN l.product_id IS NOT NULL THEN sum(p.volume * l.product_uom_qty / u.factor * u2.factor) ELSE 0 END as volume,
+ l.discount as discount,
+ CASE WHEN l.product_id IS NOT NULL THEN sum((l.price_unit * l.product_uom_qty * l.discount / 100.0 / CASE COALESCE(s.currency_rate, 0) WHEN 0 THEN 1.0 ELSE s.currency_rate END))ELSE 0 END as discount_amount,
+ s.id as order_id
+ """
+
+ for field in fields.values():
+ select_ += field
+
+ from_ = """
+ sale_order_line l
+ right outer join sale_order s on (s.id=l.order_id)
+ join res_partner partner on s.partner_id = partner.id
+ left join product_product p on (l.product_id=p.id)
+ left join product_template t on (p.product_tmpl_id=t.id)
+ left join uom_uom u on (u.id=l.product_uom)
+ left join uom_uom u2 on (u2.id=t.uom_id)
+ left join product_pricelist pp on (s.pricelist_id = pp.id)
+ %s
+ """ % from_clause
+
+ groupby_ = """
+ l.product_id,
+ l.order_id,
+ t.uom_id,
+ t.categ_id,
+ s.name,
+ s.date_order,
+ s.partner_id,
+ s.user_id,
+ s.state,
+ s.company_id,
+ s.campaign_id,
+ s.medium_id,
+ s.source_id,
+ s.pricelist_id,
+ s.analytic_account_id,
+ s.team_id,
+ p.product_tmpl_id,
+ partner.country_id,
+ partner.industry_id,
+ partner.commercial_partner_id,
+ l.discount,
+ s.id %s
+ """ % (groupby)
+
+ return '%s (SELECT %s FROM %s GROUP BY %s)' % (with_, select_, from_, groupby_)
+
+ def init(self):
+ # self._table = sale_report
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""CREATE or REPLACE VIEW %s as (%s)""" % (self._table, self._query()))
+
+class SaleOrderReportProforma(models.AbstractModel):
+ _name = 'report.sale.report_saleproforma'
+ _description = 'Proforma Report'
+
+ @api.model
+ def _get_report_values(self, docids, data=None):
+ docs = self.env['sale.order'].browse(docids)
+ return {
+ 'doc_ids': docs.ids,
+ 'doc_model': 'sale.order',
+ 'docs': docs,
+ 'proforma': True
+ }
diff --git a/addons/sale/report/sale_report.xml b/addons/sale/report/sale_report.xml
new file mode 100644
index 00000000..e3160d14
--- /dev/null
+++ b/addons/sale/report/sale_report.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <record id="action_report_saleorder" model="ir.actions.report">
+ <field name="name">Quotation / Order</field>
+ <field name="model">sale.order</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">sale.report_saleorder</field>
+ <field name="report_file">sale.report_saleorder</field>
+ <field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation - %s' % (object.name)) or 'Order - %s' % (object.name)</field>
+ <field name="binding_model_id" ref="model_sale_order"/>
+ <field name="binding_type">report</field>
+ </record>
+
+ <record id="action_report_pro_forma_invoice" model="ir.actions.report">
+ <field name="name">PRO-FORMA Invoice</field>
+ <field name="model">sale.order</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">sale.report_saleorder_pro_forma</field>
+ <field name="report_file">sale.report_saleorder_pro_forma</field>
+ <field name="print_report_name">'PRO-FORMA - %s' % (object.name)</field>
+ <field name="binding_model_id" ref="model_sale_order"/>
+ <field name="binding_type">report</field>
+ <field name="groups_id" eval="[(4, ref('sale.group_proforma_sales'))]"/>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml
new file mode 100644
index 00000000..861c257b
--- /dev/null
+++ b/addons/sale/report/sale_report_templates.xml
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<template id="report_saleorder_document">
+ <t t-call="web.external_layout">
+ <t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" />
+ <t t-set="address">
+ <div t-field="doc.partner_id"
+ t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
+ <p t-if="doc.partner_id.vat"><t t-esc="doc.company_id.country_id.vat_label or 'Tax ID'"/>: <span t-field="doc.partner_id.vat"/></p>
+ </t>
+ <t t-if="doc.partner_shipping_id == doc.partner_invoice_id
+ and doc.partner_invoice_id != doc.partner_id
+ or doc.partner_shipping_id != doc.partner_invoice_id">
+ <t t-set="information_block">
+ <strong t-if="doc.partner_shipping_id == doc.partner_invoice_id">Invoicing and Shipping Address:</strong>
+ <strong t-if="doc.partner_shipping_id != doc.partner_invoice_id">Invoicing Address:</strong>
+ <div t-field="doc.partner_invoice_id"
+ t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
+ <t t-if="doc.partner_shipping_id != doc.partner_invoice_id">
+ <strong>Shipping Address:</strong>
+ <div t-field="doc.partner_shipping_id"
+ t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
+ </t>
+ </t>
+ </t>
+ <div class="page">
+ <div class="oe_structure"/>
+
+ <h2 class="mt16">
+ <t t-if="not (env.context.get('proforma', False) or is_pro_forma)">
+ <span t-if="doc.state not in ['draft','sent']">Order # </span>
+ <span t-if="doc.state in ['draft','sent']">Quotation # </span>
+ </t>
+ <t t-if="env.context.get('proforma', False) or is_pro_forma">
+ <span>Pro-Forma Invoice # </span>
+ </t>
+ <span t-field="doc.name"/>
+ </h2>
+
+ <div class="row mt32 mb32" id="informations">
+ <div t-if="doc.client_order_ref" class="col-auto col-3 mw-100 mb-2">
+ <strong>Your Reference:</strong>
+ <p class="m-0" t-field="doc.client_order_ref"/>
+ </div>
+ <div t-if="doc.date_order and doc.state not in ['draft','sent']" class="col-auto col-3 mw-100 mb-2">
+ <strong>Order Date:</strong>
+ <p class="m-0" t-field="doc.date_order"/>
+ </div>
+ <div t-if="doc.date_order and doc.state in ['draft','sent']" class="col-auto col-3 mw-100 mb-2">
+ <strong>Quotation Date:</strong>
+ <p class="m-0" t-field="doc.date_order" t-options='{"widget": "date"}'/>
+ </div>
+ <div t-if="doc.validity_date and doc.state in ['draft', 'sent']" class="col-auto col-3 mw-100 mb-2" name="expiration_date">
+ <strong>Expiration:</strong>
+ <p class="m-0" t-field="doc.validity_date"/>
+ </div>
+ <div t-if="doc.user_id.name" class="col-auto col-3 mw-100 mb-2">
+ <strong>Salesperson:</strong>
+ <p class="m-0" t-field="doc.user_id"/>
+ </div>
+ </div>
+
+ <!-- Is there a discount on at least one line? -->
+ <t t-set="display_discount" t-value="any(l.discount for l in doc.order_line)"/>
+
+ <table class="table table-sm o_main_table">
+ <!-- In case we want to repeat the header, remove "display: table-row-group" -->
+ <thead style="display: table-row-group">
+ <tr>
+ <th name="th_description" class="text-left">Description</th>
+ <th name="th_quantity" class="text-right">Quantity</th>
+ <th name="th_priceunit" class="text-right">Unit Price</th>
+ <th name="th_discount" t-if="display_discount" class="text-right" groups="product.group_discount_per_so_line">
+ <span>Disc.%</span>
+ </th>
+ <th name="th_taxes" class="text-right">Taxes</th>
+ <th name="th_subtotal" class="text-right">
+ <span groups="account.group_show_line_subtotals_tax_excluded">Amount</span>
+ <span groups="account.group_show_line_subtotals_tax_included">Total Price</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody class="sale_tbody">
+
+ <t t-set="current_subtotal" t-value="0"/>
+
+ <t t-foreach="doc.order_line" t-as="line">
+
+ <t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
+ <t t-set="current_subtotal" t-value="current_subtotal + line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
+
+ <tr t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''">
+ <t t-if="not line.display_type">
+ <td name="td_name"><span t-field="line.name"/></td>
+ <td name="td_quantity" class="text-right">
+ <span t-field="line.product_uom_qty"/>
+ <span t-field="line.product_uom"/>
+ </td>
+ <td name="td_priceunit" class="text-right">
+ <span t-field="line.price_unit"/>
+ </td>
+ <td t-if="display_discount" class="text-right" groups="product.group_discount_per_so_line">
+ <span t-field="line.discount"/>
+ </td>
+ <td name="td_taxes" class="text-right">
+ <span t-esc="', '.join(map(lambda x: (x.description or x.name), line.tax_id))"/>
+ </td>
+ <td name="td_subtotal" class="text-right o_price_total">
+ <span t-field="line.price_subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
+ <span t-field="line.price_total" groups="account.group_show_line_subtotals_tax_included"/>
+ </td>
+ </t>
+ <t t-if="line.display_type == 'line_section'">
+ <td name="td_section_line" colspan="99">
+ <span t-field="line.name"/>
+ </td>
+ <t t-set="current_section" t-value="line"/>
+ <t t-set="current_subtotal" t-value="0"/>
+ </t>
+ <t t-if="line.display_type == 'line_note'">
+ <td name="td_note_line" colspan="99">
+ <span t-field="line.name"/>
+ </td>
+ </t>
+ </tr>
+
+ <t t-if="current_section and (line_last or doc.order_line[line_index+1].display_type == 'line_section')">
+ <tr class="is-subtotal text-right">
+ <td name="td_section_subtotal" colspan="99">
+ <strong class="mr16">Subtotal</strong>
+ <span
+ t-esc="current_subtotal"
+ t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'
+ />
+ </td>
+ </tr>
+ </t>
+ </t>
+ </tbody>
+ </table>
+
+ <div class="clearfix" name="so_total_summary">
+ <div id="total" class="row" name="total">
+ <div t-attf-class="#{'col-4' if report_type != 'html' else 'col-sm-7 col-md-5'} ml-auto">
+ <table class="table table-sm">
+ <tr class="border-black o_subtotal" style="">
+ <td name="td_amount_untaxed_label"><strong>Subtotal</strong></td>
+ <td name="td_amount_untaxed" class="text-right">
+ <span t-field="doc.amount_untaxed"/>
+ </td>
+ </tr>
+ <t t-foreach="doc.amount_by_group" t-as="amount_by_group">
+ <tr style="">
+ <t t-if="amount_by_group[5] == 1 and doc.amount_untaxed == amount_by_group[2]">
+ <td name="td_amount_by_group_label_3">
+ <span t-esc="amount_by_group[0]"/>
+ <span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/></span>
+ </td>
+ <td name="td_amount_by_group_3" class="text-right o_price_total">
+ <span t-esc="amount_by_group[1]"
+ t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/>
+ </td>
+ </t>
+ <t t-else ="">
+ <td name="td_amount_by_group_label">
+ <span t-esc="amount_by_group[0]"/>
+ </td>
+ <td name="td_amount_by_group" class="text-right o_price_total">
+ <span t-esc="amount_by_group[1]"
+ t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/>
+ </td>
+ </t>
+ </tr>
+ </t>
+ <tr class="border-black o_total">
+ <td name="td_amount_total_label"><strong>Total</strong></td>
+ <td name="td_amount_total" class="text-right">
+ <span t-field="doc.amount_total"/>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+
+ <div t-if="doc.signature" class="mt32 ml64 mr4" name="signature">
+ <div class="offset-8">
+ <strong>Signature</strong>
+ </div>
+ <div class="offset-8">
+ <img t-att-src="image_data_uri(doc.signature)" style="max-height: 4cm; max-width: 8cm;"/>
+ </div>
+ <div class="offset-8 text-center">
+ <p t-field="doc.signed_by"/>
+ </div>
+ </div>
+
+ <div class="oe_structure"/>
+
+ <p t-field="doc.note" />
+ <p t-if="doc.payment_term_id.note">
+ <span t-field="doc.payment_term_id.note"/>
+ </p>
+ <p id="fiscal_position_remark" t-if="doc.fiscal_position_id and doc.fiscal_position_id.sudo().note">
+ <strong>Fiscal Position Remark:</strong>
+ <span t-field="doc.fiscal_position_id.sudo().note"/>
+ </p>
+ </div>
+ </t>
+</template>
+
+
+<template id="report_saleorder">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="sale.report_saleorder_document" t-lang="doc.partner_id.lang"/>
+ </t>
+ </t>
+</template>
+
+<template id="report_saleorder_pro_forma">
+ <t t-call="web.html_container">
+ <t t-set="is_pro_forma" t-value="True"/>
+ <t t-foreach="docs" t-as="doc">
+ <t t-call="sale.report_saleorder_document" t-lang="doc.partner_id.lang"/>
+ </t>
+ </t>
+</template>
+
+</odoo>
diff --git a/addons/sale/report/sale_report_views.xml b/addons/sale/report/sale_report_views.xml
new file mode 100644
index 00000000..32fa2d9e
--- /dev/null
+++ b/addons/sale/report/sale_report_views.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="view_order_product_pivot" model="ir.ui.view">
+ <field name="name">sale.report.pivot</field>
+ <field name="model">sale.report</field>
+ <field name="arch" type="xml">
+ <pivot string="Sales Analysis" disable_linking="True" sample="1">
+ <field name="team_id" type="col"/>
+ <field name="date" interval="month" type="row"/>
+ <field name="price_subtotal" type="measure"/>
+ </pivot>
+ </field>
+ </record>
+
+ <record id="view_order_product_graph" model="ir.ui.view">
+ <field name="name">sale.report.graph</field>
+ <field name="model">sale.report</field>
+ <field name="arch" type="xml">
+ <graph string="Sales Analysis" type="line" sample="1" disable_linking="1">
+ <field name="date" type="row" interval="day"/>
+ <field name="price_subtotal" type="measure"/>
+ </graph>
+ </field>
+ </record>
+
+ <record id="view_order_product_search" model="ir.ui.view">
+ <field name="name">sale.report.search</field>
+ <field name="model">sale.report</field>
+ <field name="arch" type="xml">
+ <search string="Sales Analysis">
+ <field name="date"/>
+ <filter string="Date" name="year" invisible="1" date="date" default_period="this_year"/>
+ <filter name="Quotations" string="Quotations" domain="[('state','in', ('draft', 'sent'))]"/>
+ <filter name="Sales" string="Sales Orders" domain="[('state','not in',('draft', 'cancel', 'sent'))]"/>
+ <separator/>
+ <filter name="filter_date" date="date" default_period="this_month"/>
+ <separator/>
+ <field name="user_id"/>
+ <field name="team_id"/>
+ <field name="product_id"/>
+ <field name="categ_id"/>
+ <field name="partner_id"/>
+ <field name="country_id"/>
+ <field name="industry_id"/>
+ <group expand="0" string="Extended Filters">
+ <field name="categ_id" filter_domain="[('categ_id', 'child_of', self)]"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ </group>
+ <group expand="1" string="Group By">
+ <filter string="Salesperson" name="User" context="{'group_by':'user_id'}"/>
+ <filter string="Sales Team" name="sales_channel" context="{'group_by':'team_id'}"/>
+ <filter string="Customer" name="Customer" context="{'group_by':'partner_id'}"/>
+ <filter string="Customer Country" name="country_id" context="{'group_by':'country_id'}"/>
+ <filter string="Customer Industry" name="industry_id" context="{'group_by':'industry_id'}"/>
+ <filter string="Product" name="Product" context="{'group_by':'product_id'}"/>
+ <filter string="Product Category" name="Category" context="{'group_by':'categ_id'}"/>
+ <filter name="status" string="Status" context="{'group_by':'state'}"/>
+ <filter string="Company" name="company" groups="base.group_multi_company" context="{'group_by':'company_id'}"/>
+ <separator/>
+ <filter string="Order Date" name="date" context="{'group_by':'date'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="action_order_report_all" model="ir.actions.act_window">
+ <field name="name">Sales Analysis</field>
+ <field name="res_model">sale.report</field>
+ <field name="view_mode">graph,pivot</field>
+ <field name="view_id"></field> <!-- force empty -->
+ <field name="search_view_id" ref="view_order_product_search"/>
+ <field name="context">{'search_default_Sales':1, 'group_by_no_leaf':1,'group_by':[]}</field>
+ <field name="help">This report performs analysis on your quotations and sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
+ </record>
+</odoo>