19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.tools import SQL
class AccountInvoiceReport(models.Model):
@ -9,5 +9,5 @@ class AccountInvoiceReport(models.Model):
team_id = fields.Many2one(comodel_name='crm.team', string="Sales Team")
def _select(self):
return super()._select() + ", move.team_id as team_id"
def _select(self) -> SQL:
return SQL("%s, move.team_id as team_id", super()._select())

View file

@ -16,7 +16,7 @@
</record>
<record id="account_invoice_report_view_tree" model="ir.ui.view">
<field name="name">account.invoice.report.view.tree.inherit.sale</field>
<field name="name">account.invoice.report.view.list.inherit.sale</field>
<field name="model">account.invoice.report</field>
<field name="inherit_id" ref="account.account_invoice_report_view_tree"/>
<field name="arch" type="xml">

View file

@ -21,7 +21,7 @@
<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'))]"/>
<field name="group_ids" eval="[(4, ref('sale.group_proforma_sales'))]"/>
</record>
</odoo>

View file

@ -5,9 +5,12 @@
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)" />
<t t-set="forced_vat" t-value="doc.fiscal_position_id.foreign_vat"/> <!-- So that it appears in the footer of the report instead of the company VAT if it's set -->
<t t-set="address">
<div t-field="doc.partner_id"
<div t-field="doc.partner_id" class="mb-0"
t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
<p t-if="doc.partner_id.vat"><t t-out="doc.company_id.account_fiscal_country_id.vat_label or 'Tax ID'"/>: <span t-field="doc.partner_id.vat"/></p>
<p t-if="doc.partner_id.vat" class="mb-0">
<t t-if="doc.company_id.account_fiscal_country_id.vat_label" t-out="doc.company_id.account_fiscal_country_id.vat_label"/>
<t t-else="">Tax ID</t>: <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
@ -15,16 +18,16 @@
<t t-set="information_block">
<strong>
<t t-if="doc.partner_shipping_id == doc.partner_invoice_id">
Invoicing and Shipping Address:
Invoicing and Shipping Address
</t>
<t t-else="">
Invoicing Address:
Invoicing Address
</t>
</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>
<strong class="d-block mt-3">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>
@ -33,129 +36,288 @@
<div class="page">
<div class="oe_structure"/>
<h2 class="mt-4">
<span t-if="env.context.get('proforma', False) or is_pro_forma">Pro-Forma Invoice # </span>
<t t-set="is_proforma" t-value="env.context.get('proforma', False) or is_pro_forma"/>
<t t-set="layout_document_title">
<span t-if="is_proforma">Pro-Forma Invoice # </span>
<span t-elif="doc.state in ['draft','sent']">Quotation # </span>
<span t-else="">Order # </span>
<span t-field="doc.name"/>
</h2>
<span t-field="doc.name">SO0000</span>
</t>
<div class="row mt-4 mb-4" id="informations">
<div t-if="doc.client_order_ref" class="col-auto col-3 mw-100 mb-2" name="informations_reference">
<strong>Your Reference:</strong>
<p class="m-0" t-field="doc.client_order_ref"/>
<div class="row mb-4" id="informations">
<div t-if="doc.client_order_ref" class="col" name="informations_reference">
<strong>Your Reference</strong>
<div t-field="doc.client_order_ref">SO0000</div>
</div>
<div t-if="doc.date_order" class="col-auto col-3 mw-100 mb-2" name="informations_date">
<strong t-if="doc.state in ['draft', 'sent']">Quotation Date:</strong>
<strong t-else="">Order Date:</strong>
<p class="m-0" t-field="doc.date_order" t-options='{"widget": "date"}'/>
<div t-if="doc.date_order" class="col" name="informations_date">
<strong t-if="is_proforma">Issued Date</strong>
<strong t-elif="doc.state in ['draft', 'sent']">Quotation Date</strong>
<strong t-else="">Order Date</strong>
<div t-field="doc.date_order" t-options='{"widget": "date"}'>2023-12-31</div>
</div>
<div t-if="doc.validity_date and doc.state in ['draft', 'sent']"
class="col-auto col-3 mw-100 mb-2"
class="col"
name="expiration_date">
<strong>Expiration:</strong>
<p class="m-0" t-field="doc.validity_date"/>
<strong>Expiration</strong>
<div t-field="doc.validity_date">2023-12-31</div>
</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 t-if="doc.commitment_date" class="col" name="delivery_date">
<strong>Delivery Date</strong>
<div
t-field="doc.commitment_date"
t-options="{'widget': 'date'}"
>
2023-12-31
</div>
</div>
<div t-if="doc.user_id.name" class="col">
<strong>Salesperson</strong>
<div t-field="doc.user_id">Mitchell Admin</div>
</div>
</div>
<!-- Is there a discount on at least one line? -->
<t t-set="lines_to_report" t-value="doc._get_order_lines_to_report()"/>
<t t-set="display_discount" t-value="any(l.discount for l in lines_to_report)"/>
<!-- Hide taxes AND show taxed amounts on lines -->
<t t-set="hide_taxes_details" t-value="False"/>
<t t-set="display_taxes" t-value="not hide_taxes_details and any(l._has_taxes() for l in lines_to_report)"/>
<table class="table table-sm o_main_table table-borderless mt-4">
<div class="oe_structure"></div>
<table class="o_has_total_table table o_main_table table-borderless">
<!-- 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-start">Description</th>
<th name="th_quantity" class="text-end">Quantity</th>
<th name="th_priceunit" class="text-end">Unit Price</th>
<th name="th_quantity" class="text-end text-nowrap">Quantity</th>
<th name="th_priceunit" class="text-end text-nowrap">Unit Price</th>
<th name="th_discount" t-if="display_discount" class="text-end">
<span>Disc.%</span>
</th>
<th name="th_taxes" class="text-end">Taxes</th>
<th name="th_taxes" t-if="display_taxes" class="text-end">
<span>Taxes</span>
</th>
<th name="th_subtotal" class="text-end">
<span groups="account.group_show_line_subtotals_tax_excluded">Amount</span>
<span groups="account.group_show_line_subtotals_tax_included">Total Price</span>
<span>Amount</span>
</th>
</tr>
</thead>
<tbody class="sale_tbody">
<t t-set="current_subtotal" t-value="0"/>
<t t-set="current_section" t-value="None"/>
<t t-set="current_subsection" t-value="None"/>
<t t-set="price_field" t-value="'price_total' if hide_taxes_details else 'price_subtotal'"/>
<t t-foreach="lines_to_report" 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"/>
<t t-set="is_note" t-value="line.display_type == 'line_note'"/>
<t t-set="is_section" t-value="line.display_type == 'line_section'"/>
<t t-set="is_subsection" t-value="line.display_type == 'line_subsection'"/>
<t t-set="is_combo" t-value="line.product_type == 'combo'"/>
<tr t-att-class="'bg-200 fw-bold o_line_section' if line.display_type == 'line_section' else 'fst-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-end">
<span t-field="line.product_uom_qty"/>
<span t-field="line.product_uom"/>
<t t-if="is_section">
<t t-set="current_section" t-value="line"/>
<t t-set="current_subsection" t-value="None"/>
<t t-set="line_padding" t-value="0"/>
<t t-set="collapse_prices" t-value="line.collapse_prices"/>
</t>
<t t-elif="is_subsection">
<t t-set="current_subsection" t-value="line"/>
<t t-set="line_padding" t-value="2"/>
<t t-set="collapse_prices" t-value="collapse_prices or line.collapse_prices"/>
</t>
<t t-else="">
<t
t-set="line_padding"
t-value="3 if current_subsection else (2 if current_section else 0)"
/>
</t>
<t t-if="line.combo_item_id">
<t t-set="line_padding" t-value="line_padding + 1"/>
</t>
<t t-set="padding_class" t-value="line_padding and ('px-' + str(line_padding)) or ''"/>
<t t-if="not line.collapse_composition">
<tr
t-if="is_section or is_subsection"
name="tr_section"
t-att-class="'fw-bolder o_line_section' if is_section else 'fw-bold o_line_subsection'"
>
<t t-set="show_section_total" t-value="is_section or not line.parent_id.collapse_prices"/>
<t
t-set="section_name_colspan"
t-value="3 + (1 if display_discount else 0) + (1 if display_taxes else 0)"
/>
<t t-if="not show_section_total">
<t t-set="section_name_colspan" t-value="99"/>
</t>
<td
name="td_section_name"
t-att-colspan="section_name_colspan"
t-att-class="padding_class"
>
<span t-field="line.name">A section title</span>
</td>
<td name="td_priceunit" class="text-end">
<span t-field="line.price_unit"/>
<td t-if="show_section_total" name="td_section_price" class="text-end o_price_total">
<span
name="price_subtotal_section"
t-out="line._get_section_totals(price_field)"
class="text-nowrap"
t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"
>
27.00
</span>
</td>
<td t-if="display_discount" class="text-end">
<span t-field="line.discount"/>
</tr>
<tr t-elif="is_note" name="tr_note" class="fst-italic o_line_note">
<td
name="td_note_name"
colspan="99"
t-att-class="padding_class"
>
<span t-field="line.name">A note, whose content usually applies to the section or product above.</span>
</td>
<t t-set="taxes" t-value="', '.join([(tax.description or tax.name) for tax in line.tax_id])"/>
<td name="td_taxes" t-attf-class="text-end {{ 'text-nowrap' if len(taxes) &lt; 10 else '' }}">
</tr>
<tr t-elif="is_combo" name="tr_combo" class="fw-bold o_line_subsection">
<td
name="td_combo_name"
t-att-colspan="3 + (1 if display_discount else 0) + (1 if display_taxes else 0)"
t-att-class="padding_class"
>
<span t-field="line.name">Combo Product</span>
x <span t-field="line.product_uom_qty" t-options="{'widget': 'integer'}"/>
</td>
<td name="td_combo_price" class="text-end o_price_total">
<span
t-if="not collapse_prices"
name="price_subtotal_combo"
t-out="line._get_combo_totals(price_field)"
class="text-nowrap"
t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"
>
27.00
</span>
</td>
</tr>
<tr t-else="" name="tr_product">
<td
name="td_product_name"
t-att-class="padding_class"
>
<span t-field="line.name">Bacon Burger</span>
</td>
<td name="td_product_quantity" class="o_td_quantity text-end">
<span t-field="line.product_uom_qty" class="text-nowrap">3</span>
<span t-field="line.product_uom_id">units</span>
<span t-if="line.product_uom_id != line.product_id.uom_id" class="text-muted small">
<t t-set="quantity_in_product_uom" t-value="line.product_uom_id._compute_quantity(line.product_uom_qty, line.product_id.uom_id)"/>
<br/><span t-esc="quantity_in_product_uom" t-options="{'widget': 'float', 'decimal_precision': 'Product Unit'}"/> <span t-field="line.product_id.uom_id"/>
</span>
</td>
<td name="td_product_priceunit" class="text-end text-nowrap">
<span
t-if="not collapse_prices"
t-field="line.price_unit"
>
25.00
</span>
</td>
<td t-if="display_discount" name="td_product_discount" class="text-end">
<t t-if="not collapse_prices">
<!-- ! Always check collapse_prices separately; keep discount condition below for XPath -->
<span t-field="line.discount">-</span>
</t>
</td>
<t t-set="taxes" t-value="', '.join(tax.tax_label for tax in line.tax_ids if tax.tax_label)"/>
<td t-if="display_taxes" name="td_product_taxes" t-attf-class="text-end {{ 'text-nowrap' if len(taxes) &lt; 10 else '' }}">
<span t-out="taxes">Tax 15%</span>
</td>
<td t-if="not line.is_downpayment" name="td_subtotal" class="text-end 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-if="not line.is_downpayment" name="td_product_subtotal" class="text-end o_price_total">
<t t-if="not collapse_prices">
<span
t-if="price_field == 'price_subtotal'"
t-field="line.price_subtotal"
>
27.00
</span>
<span t-else="" t-field="line.price_total">
27.00
</span>
</t>
</td>
</t>
<t t-elif="line.display_type == 'line_section'">
<td name="td_section_line" colspan="99">
<span t-field="line.name"/>
</tr>
</t>
<t t-else="">
<!-- Displaying the section inner content, grouped by taxes -->
<tr
t-foreach="line._get_grouped_section_summary(display_taxes)"
t-as="section_line"
name="tr_section_group"
class="o_line_section"
>
<td name="td_section_group_name" t-att-class="padding_class">
<span t-field="line.name">
Section Summary
</span>
</td>
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
</t>
<t t-elif="line.display_type == 'line_note'">
<td name="td_note_line" colspan="99">
<span t-field="line.name"/>
<td name="td_section_group_quantity" class="text-end text-nowrap">
<span>1.00 Units</span>
</td>
</t>
</tr>
<t t-if="current_section and (line_last or lines_to_report[line_index+1].display_type == 'line_section') and not line.is_downpayment">
<tr class="is-subtotal text-end">
<td name="td_section_subtotal" colspan="99">
<strong class="mr16">Subtotal</strong>
<td name="td_section_group_priceunit" class="text-end text-nowrap">
<span
t-out="current_subtotal"
t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'
/>
name="price_subtotal_section_grouped"
t-out="section_line['price_subtotal']"
t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"
>
25.00
</span>
</td>
<td t-if="display_discount" name="td_section_group_discount">
<!-- NOTE: not computed atm, but at least provides the API for it if needed in the future -->
<t t-set="aggregated_discount" t-value="section_line.get('discount', 0)"/>
<span t-if="aggregated_discount" t-out="aggregated_discount">-</span>
</td>
<td
t-if="display_taxes"
name="td_section_group_taxes"
t-attf-class="text-end {{ 'text-nowrap' if len(section_line['tax_labels']) &lt; 10 else '' }}"
>
<span t-out="', '.join(section_line['tax_labels'])">
Tax 15%
</span>
</td>
<td name="td_section_group_total" class="text-end o_price_total">
<span
t-out="section_line[price_field]"
t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"
>
30.00
</span>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<div class="clearfix" name="so_total_summary">
<div id="total" class="row" name="total">
<div id="total" class="row mt-n3" name="total">
<div t-attf-class="#{'col-6' if report_type != 'html' else 'col-sm-7 col-md-6'} ms-auto">
<table class="table table-sm table-borderless">
<table class="o_total_table table table-borderless">
<!-- Tax totals -->
<t t-set="tax_totals" t-value="doc.tax_totals"/>
<t t-call="account.document_tax_totals"/>
<t t-call="sale.document_tax_totals">
<t t-set="tax_totals" t-value="doc.tax_totals"/>
<t t-set="currency" t-value="doc.currency_id"/>
</t>
</table>
</div>
</div>
</div>
<div class="oe_structure"></div>
<div t-if="doc.signature" class="mt-4 ml64 mr4" name="signature">
<div t-if="not doc.signature" class="oe_structure"></div>
<div t-else="" class="mt-4 ml64 mr4" name="signature">
<div class="offset-8">
<strong>Signature</strong>
</div>
@ -163,27 +325,32 @@
<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"/>
<span t-field="doc.signed_by">Oscar Morgan</span>
</div>
</div>
<div>
<p t-field="doc.note" name="order_note"/>
<span t-field="doc.note" t-attf-style="#{'text-align:justify;text-justify:inter-word;' if doc.company_id.terms_type != 'html' else ''}" name="order_note"/>
<p t-if="not is_html_empty(doc.payment_term_id.note)">
<span t-field="doc.payment_term_id.note"/>
<span t-field="doc.payment_term_id.note">The payment should also be transmitted with love</span>
</p>
<div class="oe_structure"/>
<p t-if="doc.fiscal_position_id and not is_html_empty(doc.fiscal_position_id.sudo().note)"
id="fiscal_position_remark">
<strong>Fiscal Position Remark:</strong>
<span t-field="doc.fiscal_position_id.sudo().note"/>
<span t-field="doc.fiscal_position_id.sudo().note">No further requirements for this payment</span>
</p>
</div>
<div class="oe_structure"/>
<t t-set="base_address" t-value="doc.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="portal_url" t-value="base_address + '/my/orders/' + str(doc.id) + '#portal_connect_software_modal_btn'"/>
<div t-if="any(u._is_portal() for u in doc.partner_id.user_ids) and doc._get_edi_builders()" class="text-center">
<a t-att-href="portal_url">Connect your software</a> with <t t-out="doc.company_id.name"/> to create quotes automatically.
</div>
</div>
</t>
</template>
<template id="report_saleorder">
<template id="report_saleorder_raw">
<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"/>
@ -191,6 +358,10 @@
</t>
</template>
<template id="report_saleorder">
<t t-call="sale.report_saleorder_raw"/>
</template>
<template id="report_saleorder_pro_forma">
<t t-call="web.html_container">
<t t-set="is_pro_forma" t-value="True"/>
@ -201,4 +372,13 @@
</t>
</template>
<!-- Allow edits (e.g. studio) without changing the often inherited base template -->
<template id="document_tax_totals" inherit_id="account.document_tax_totals_template" primary="True"></template>
<template id="quote_document_layout_preview">
<t t-call="web.html_preview_container">
<t t-call="sale.report_saleorder_document"/>
</t>
</template>
</odoo>

View file

@ -2,9 +2,11 @@
from odoo import api, fields, models
from odoo.addons.sale.models.sale_order import SALE_ORDER_STATE
class SaleReport(models.Model):
_name = "sale.report"
_name = 'sale.report'
_description = "Sales Analysis Report"
_auto = False
_rec_name = 'date'
@ -12,50 +14,79 @@ class SaleReport(models.Model):
@api.model
def _get_done_states(self):
return ['sale', 'done']
return ['sale']
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_to_deliver = fields.Float('Qty To Deliver', 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)
# sale.order fields
name = fields.Char(string="Order Reference", readonly=True)
date = fields.Datetime(string="Order Date", readonly=True)
partner_id = fields.Many2one(comodel_name='res.partner', string="Customer", readonly=True)
company_id = fields.Many2one(comodel_name='res.company', readonly=True)
pricelist_id = fields.Many2one(comodel_name='product.pricelist', readonly=True)
team_id = fields.Many2one(comodel_name='crm.team', string="Sales Team", readonly=True)
user_id = fields.Many2one(comodel_name='res.users', string="Salesperson", readonly=True)
state = fields.Selection(selection=SALE_ORDER_STATE, string="Status", readonly=True)
invoice_status = fields.Selection(
selection=[
('upselling', "Upselling Opportunity"),
('invoiced', "Fully Invoiced"),
('to invoice', "To Invoice"),
('no', "Nothing to Invoice"),
], string="Order Invoice Status", readonly=True)
discount = fields.Float('Discount %', readonly=True, group_operator="avg")
discount_amount = fields.Float('Discount Amount', readonly=True)
campaign_id = fields.Many2one('utm.campaign', 'Campaign', readonly=True)
medium_id = fields.Many2one('utm.medium', 'Medium', readonly=True)
source_id = fields.Many2one('utm.source', 'Source', readonly=True)
campaign_id = fields.Many2one(comodel_name='utm.campaign', string="Campaign", readonly=True)
medium_id = fields.Many2one(comodel_name='utm.medium', string="Medium", readonly=True)
source_id = fields.Many2one(comodel_name='utm.source', string="Source", readonly=True)
order_id = fields.Many2one('sale.order', 'Order #', readonly=True)
# res.partner fields
commercial_partner_id = fields.Many2one(
comodel_name='res.partner', string="Customer Entity", readonly=True)
country_id = fields.Many2one(
comodel_name='res.country', string="Customer Country", readonly=True)
industry_id = fields.Many2one(
comodel_name='res.partner.industry', string="Customer Industry", readonly=True)
partner_zip = fields.Char(string="Customer ZIP", readonly=True)
state_id = fields.Many2one(comodel_name='res.country.state', string="Customer State", readonly=True)
# sale.order.line fields
order_reference = fields.Reference(
string='Order',
selection=[('sale.order', 'Sales Order')],
aggregator="count_distinct",
)
categ_id = fields.Many2one(
comodel_name='product.category', string="Product Category", readonly=True)
product_id = fields.Many2one(
comodel_name='product.product', string="Product Variant", readonly=True)
product_tmpl_id = fields.Many2one(
comodel_name='product.template', string="Product", readonly=True)
product_uom_id = fields.Many2one(comodel_name='uom.uom', string="Unit", readonly=True)
product_uom_qty = fields.Float(string="Qty Ordered", readonly=True)
qty_to_deliver = fields.Float(string="Qty To Deliver", readonly=True)
qty_delivered = fields.Float(string="Qty Delivered", readonly=True)
qty_to_invoice = fields.Float(string="Qty To Invoice", readonly=True)
qty_invoiced = fields.Float(string="Qty Invoiced", readonly=True)
price_subtotal = fields.Monetary(string="Untaxed Total", readonly=True)
price_total = fields.Monetary(string="Total", readonly=True)
untaxed_amount_to_invoice = fields.Monetary(string="Untaxed Amount To Invoice", readonly=True)
untaxed_amount_invoiced = fields.Monetary(string="Untaxed Amount Invoiced", readonly=True)
line_invoice_status = fields.Selection(
selection=[
('upselling', "Upselling Opportunity"),
('invoiced', "Fully Invoiced"),
('to invoice', "To Invoice"),
('no', "Nothing to Invoice"),
], string="Invoice Status", readonly=True)
weight = fields.Float(string="Gross Weight", readonly=True)
volume = fields.Float(string="Volume", readonly=True)
price_unit = fields.Float(string="Unit Price", aggregator='avg', readonly=True)
discount = fields.Float(string="Discount %", readonly=True, aggregator='avg')
discount_amount = fields.Monetary(string="Discount Amount", readonly=True)
# aggregates or computed fields
nbr = fields.Integer(string="# of Lines", readonly=True)
currency_id = fields.Many2one(comodel_name='res.currency', readonly=True)
def _with_sale(self):
return ""
@ -64,36 +95,43 @@ class SaleReport(models.Model):
select_ = f"""
MIN(l.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.product_uom_qty - l.qty_delivered) / u.factor * u2.factor) ELSE 0 END AS qty_to_deliver,
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,
l.invoice_status AS line_invoice_status,
t.uom_id AS product_uom_id,
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.product_uom_qty - l.qty_delivered) * u.factor / u2.factor) ELSE 0 END AS qty_to_deliver,
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 AVG(l.price_unit
/ {self._case_value_or_one('s.currency_rate')}
* {self._case_value_or_one('account_currency_table.rate')}
) ELSE 0
END AS price_unit,
CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_total
/ {self._case_value_or_one('s.currency_rate')}
* {self._case_value_or_one('currency_table.rate')}
* {self._case_value_or_one('account_currency_table.rate')}
) ELSE 0
END AS price_total,
CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_subtotal
/ {self._case_value_or_one('s.currency_rate')}
* {self._case_value_or_one('currency_table.rate')}
* {self._case_value_or_one('account_currency_table.rate')}
) ELSE 0
END AS price_subtotal,
CASE WHEN l.product_id IS NOT NULL THEN SUM(l.untaxed_amount_to_invoice
CASE WHEN l.product_id IS NOT NULL OR l.is_downpayment THEN SUM(l.untaxed_amount_to_invoice
/ {self._case_value_or_one('s.currency_rate')}
* {self._case_value_or_one('currency_table.rate')}
* {self._case_value_or_one('account_currency_table.rate')}
) ELSE 0
END AS untaxed_amount_to_invoice,
CASE WHEN l.product_id IS NOT NULL THEN SUM(l.untaxed_amount_invoiced
CASE WHEN l.product_id IS NOT NULL OR l.is_downpayment THEN SUM(l.untaxed_amount_invoiced
/ {self._case_value_or_one('s.currency_rate')}
* {self._case_value_or_one('currency_table.rate')}
* {self._case_value_or_one('account_currency_table.rate')}
) ELSE 0
END AS untaxed_amount_invoiced,
COUNT(*) AS nbr,
s.name AS name,
s.date_order AS date,
s.state AS state,
s.invoice_status as invoice_status,
s.partner_id AS partner_id,
s.user_id AS user_id,
s.company_id AS company_id,
@ -102,21 +140,23 @@ class SaleReport(models.Model):
s.source_id AS source_id,
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.commercial_partner_id AS commercial_partner_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,
partner.state_id AS state_id,
partner.zip AS partner_zip,
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
/ {self._case_value_or_one('s.currency_rate')}
* {self._case_value_or_one('currency_table.rate')}
* {self._case_value_or_one('account_currency_table.rate')}
) ELSE 0
END AS discount_amount,
s.id AS order_id"""
{self.env.company.currency_id.id} AS currency_id,
concat('sale.order', ',', s.id) AS order_reference"""
additional_fields_info = self._select_additional_fields()
template = """,
@ -138,22 +178,18 @@ class SaleReport(models.Model):
return {}
def _from_sale(self):
return """
currency_table = self.env['res.currency']._get_simple_currency_table(self.env.companies)
currency_table = self.env.cr.mogrify(currency_table).decode(self.env.cr.connection.encoding)
return f"""
sale_order_line l
LEFT 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 u ON u.id=l.product_uom_id
LEFT JOIN uom_uom u2 ON u2.id=t.uom_id
JOIN {currency_table} ON currency_table.company_id = s.company_id
""".format(
currency_table=self.env['res.currency']._get_query_currency_table(
{
'multi_company': True,
'date': {'date_to': fields.Date.today()}
}),
)
JOIN {currency_table} ON account_currency_table.company_id = s.company_id
"""
def _where_sale(self):
return """
@ -163,6 +199,8 @@ class SaleReport(models.Model):
return """
l.product_id,
l.order_id,
l.price_unit,
l.invoice_status,
t.uom_id,
t.categ_id,
s.name,
@ -170,20 +208,23 @@ class SaleReport(models.Model):
s.partner_id,
s.user_id,
s.state,
s.invoice_status,
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.commercial_partner_id,
partner.country_id,
partner.industry_id,
partner.commercial_partner_id,
partner.state_id,
partner.zip,
l.is_downpayment,
l.discount,
s.id,
currency_table.rate"""
account_currency_table.rate"""
def _query(self):
with_ = self._with_sale()
@ -199,3 +240,13 @@ class SaleReport(models.Model):
@property
def _table_query(self):
return self._query()
@api.readonly
def action_open_order(self):
self.ensure_one()
return {
'res_model': self.order_reference._name,
'type': 'ir.actions.act_window',
'views': [[False, 'form']],
'res_id': self.order_reference.id,
}

View file

@ -8,7 +8,7 @@
<pivot string="Sales Analysis" sample="1">
<field name="team_id" type="col"/>
<field name="date" interval="month" type="row"/>
<field name="price_subtotal" type="measure"/>
<field name="product_uom_qty" type="measure"/>
</pivot>
</field>
</record>
@ -18,27 +18,58 @@
<field name="model">sale.report</field>
<field name="arch" type="xml">
<graph string="Sales Analysis" type="line" sample="1">
<field name="date" interval="day"/>
<field name="price_subtotal" type="measure"/>
<field name="date" interval="month"/>
<field name="product_uom_qty" type="measure"/>
</graph>
</field>
</record>
<record id="sale_report_graph_pie" model="ir.ui.view">
<field name="name">sale.report.graph.pie</field>
<field name="model">sale.report</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_order_product_graph"/>
<field name="arch" type="xml">
<graph position="attributes">
<attribute name="type">pie</attribute>
</graph>
</field>
</record>
<record id="sale_report_graph_bar" model="ir.ui.view">
<field name="name">sale.report.graph.bar</field>
<field name="model">sale.report</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_order_product_graph"/>
<field name="arch" type="xml">
<graph position="attributes">
<attribute name="type">bar</attribute>
<attribute name="order">DESC</attribute>
</graph>
</field>
</record>
<record id="sale_report_view_tree" model="ir.ui.view">
<field name="name">sale.report.view.tree</field>
<field name="name">sale.report.view.list</field>
<field name="model">sale.report</field>
<field name="arch" type="xml">
<tree string="Sales Analysis">
<field name="date" widget="date"/>
<field name="order_id" optional="show"/>
<field name="partner_id" optional="hide"/>
<list string="Sales Analysis" action="action_open_order" type="object">
<field name="date"/>
<field name="order_reference" optional="show"/>
<field name="product_id" string="Product" optional="show"/>
<field name="partner_id"/>
<field name="user_id" optional="show" widget="many2one_avatar_user"/>
<field name="team_id" optional="show"/>
<field name="team_id" optional="hide"/>
<field name="company_id" optional="show" groups="base.group_multi_company"/>
<field name="product_uom_qty" string="Quantity" sum="Sum of Quantity"/>
<field name="price_subtotal" optional="hide" sum="Sum of Untaxed Total"/>
<field name="price_unit" widget="monetary" avg="Average"/>
<field name="price_total" optional="show" sum="Sum of Total"/>
<field name="state" optional="hide"/>
</tree>
<field name="pricelist_id" optional="hide"/>
<field name="line_invoice_status" optional="hide"/>
<field name="currency_id" column_invisible="True"/>
</list>
</field>
</record>
@ -48,36 +79,41 @@
<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 string="Date" name="year" invisible="1" date="date" default_period="year"/>
<filter string="Quotations" name="Quotations" domain="[('state','in', ('draft', 'sent'))]"/>
<filter string="Sales Orders" name="Sales" domain="[('state','not in',('draft', 'cancel', 'sent'))]"/>
<separator/>
<filter name="filter_date" date="date" default_period="this_month"/>
<filter name="filter_order_date" invisible="1" string="Order Date: Last 365 Days" domain="[('date', '&gt;=', (datetime.datetime.combine(context_today() + relativedelta(days=-365), datetime.time(0,0,0))).strftime('%Y-%m-%d %H:%M:%S'))]"/>
<filter name="filter_date" date="date" default_period="month"/>
<filter name="filter_order_date" invisible="1" string="Order Date: Last 365 Days" domain="[('date', '&gt;=', '-365d')]"/>
<separator/>
<field name="user_id"/>
<field name="team_id"/>
<field name="product_id"/>
<field name="product_tmpl_id"/>
<field name="categ_id"/>
<filter name="to_invoice" string="To Invoice" domain="[('line_invoice_status', '=', 'to invoice')]"/>
<filter name="fully_invoiced" string="Fully Invoiced" domain="[('line_invoice_status', '=', 'invoiced')]"/>
<field name="partner_id"/>
<field name="country_id"/>
<field name="industry_id"/>
<group expand="0" string="Extended Filters">
<group>
<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">
<group>
<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" name="product_tmpl_id" context="{'group_by':'product_tmpl_id'}"/>
<filter string="Product Variant" name="product_id" context="{'group_by':'product_id'}"
groups="product.group_product_variant"/>
<filter string="Product Category" name="Category" context="{'group_by':'categ_id'}"/>
<filter string="Status" name="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'}"
<filter string="Order Date" name="group_by_date" context="{'group_by':'date'}"
invisible="context.get('sale_report_view_hide_date')"/>
<filter string="Order Date" name="group_by_date_day" context="{'group_by':'date:day'}"
invisible="not context.get('sale_report_view_hide_date')"/>
@ -89,24 +125,54 @@
<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,tree</field>
<field name="view_mode">graph,pivot,list,form</field>
<field name="view_id"></field> <!-- force empty -->
<field name="search_view_id" ref="view_order_product_search"/>
<field name="domain">[('state', '!=', 'cancel')]</field>
<field name="context">{'search_default_Sales':1, 'group_by_no_leaf':1,'group_by':[], 'search_default_filter_order_date': 1}</field>
<field name="context">{'search_default_Sales':1,'group_by':[], 'search_default_filter_order_date': 1}</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>
<record id="action_order_report_salesperson" model="ir.actions.act_window">
<field name="name">Sales Analysis By Salespersons</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph,pivot</field>
<field name="view_id" ref="sale_report_graph_bar"/>
<field name="search_view_id" ref="view_order_product_search"/>
<field name="context">{'search_default_User': 1, 'group_by': 'user_id', 'search_default_filter_order_date': 1}</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>
<record id="action_order_report_products" model="ir.actions.act_window">
<field name="name">Sales Analysis By Products</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph,pivot</field>
<field name="view_id" ref="sale_report_graph_pie"/>
<field name="search_view_id" ref="view_order_product_search"/>
<field name="context">{'search_default_Sales': 1, 'search_default_Product': 1, 'group_by': 'product_id', 'search_default_filter_order_date': 1}</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>
<record id="action_order_report_customers" model="ir.actions.act_window">
<field name="name">Sales Analysis By Customers</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph,pivot</field>
<field name="view_id" ref="sale_report_graph_bar"/>
<field name="search_view_id" ref="view_order_product_search"/>
<field name="context">{'search_default_Customer': 1, 'group_by': 'partner_id', 'search_default_filter_order_date': 1}</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>
<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,tree,graph</field>
<field name="view_mode">list,pivot,graph,form</field>
</record>
<record id="action_order_report_quotation_salesteam" model="ir.actions.act_window">
<field name="name">Quotations Analysis</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph,tree</field>
<field name="view_mode">graph,list</field>
<field name="domain">[('state','=','draft'),('team_id', '=', active_id)]</field>
<field name="context">{'search_default_order_month':1}</field>
<field name="help">This report performs analysis on your quotations. 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>
@ -115,7 +181,7 @@
<record id="action_order_report_so_salesteam" model="ir.actions.act_window">
<field name="name">Sales Analysis</field>
<field name="res_model">sale.report</field>
<field name="view_mode">graph,tree</field>
<field name="view_mode">graph,list</field>
<field name="domain">[('state','not in',('draft','cancel'))]</field>
<field name="context">{
'search_default_Sales': 1,