19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, tools
from odoo import api, fields, models, tools
from odoo.tools import formatLang
class PurchaseBillUnion(models.Model):
_name = 'purchase.bill.union'
_auto = False
@ -30,19 +31,20 @@ class PurchaseBillUnion(models.Model):
id as vendor_bill_id, NULL as purchase_order_id
FROM account_move
WHERE
move_type='in_invoice' and state = 'posted'
move_type in ('in_invoice', 'in_refund') and state = 'posted'
UNION
SELECT
-id, name, partner_ref as reference, partner_id, date_order::date as date, amount_untaxed as amount, currency_id, company_id,
NULL as vendor_bill_id, id as purchase_order_id
FROM purchase_order
WHERE
state in ('purchase', 'done') AND
state = 'purchase' AND
invoice_status in ('to invoice', 'no')
)""")
def name_get(self):
result = []
@api.depends('currency_id', 'reference', 'amount', 'purchase_order_id')
@api.depends_context('show_total_amount')
def _compute_display_name(self):
for doc in self:
name = doc.name or ''
if doc.reference:
@ -50,6 +52,5 @@ class PurchaseBillUnion(models.Model):
amount = doc.amount
if doc.purchase_order_id and doc.purchase_order_id.invoice_status == 'no':
amount = 0.0
name += ': ' + formatLang(self.env, amount, monetary=True, currency_obj=doc.currency_id)
result.append((doc.id, name))
return result
name += ': ' + formatLang(self.env, amount, currency_obj=doc.currency_id)
doc.display_name = name

View file

@ -18,18 +18,18 @@
</record>
<record id="view_purchase_bill_union_tree" model="ir.ui.view">
<field name="name">purchase.bill.union.tree</field>
<field name="name">purchase.bill.union.list</field>
<field name="model">purchase.bill.union</field>
<field name="arch" type="xml">
<tree string="Reference Document">
<list string="Reference Document">
<field name="name"/>
<field name="reference"/>
<field name="partner_id"/>
<field name="date"/>
<field name="amount"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="True"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</tree>
</list>
</field>
</record>

View file

@ -9,7 +9,7 @@
</t>
<t t-if="o.dest_address_id">
<t t-set="information_block">
<strong>Shipping address:</strong>
<strong class="d-block mt-3">Shipping address</strong>
<div t-if="o.dest_address_id">
<div t-field="o.dest_address_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}' name="purchase_shipping_address"/>
@ -21,72 +21,82 @@
<div class="oe_structure"/>
<div class="mt-4">
<h2 t-if="o.state in ['draft', 'sent', 'to approve']">Request for Quotation #<span t-field="o.name"/></h2>
<h2 t-if="o.state in ['purchase', 'done']">Purchase Order #<span t-field="o.name"/></h2>
<h2 t-if="o.state == 'cancel'">Cancelled Purchase Order #<span t-field="o.name"/></h2>
<t t-set="layout_document_title">
<t t-if="o.state in ['draft', 'sent', 'to approve']">Request for Quotation #<span t-field="o.name"/></t>
<t t-if="o.state == 'purchase'">Purchase Order #<span t-field="o.name"/></t>
<t t-if="o.state == 'cancel'">Cancelled Purchase Order #<span t-field="o.name"/></t>
</t>
</div>
<div id="informations" class="row mt-4 mb32">
<div t-if="o.user_id" class="col-3 bm-2">
<strong>Purchase Representative:</strong>
<p t-field="o.user_id" class="m-0"/>
<div id="informations" class="row mb-4">
<div t-if="o.user_id" class="col">
<strong>Buyer</strong>
<div t-field="o.user_id"/>
</div>
<div t-if="o.partner_ref" class="col-3 bm-2">
<strong>Your Order Reference:</strong>
<p t-field="o.partner_ref" class="m-0"/>
<div t-if="o.partner_ref" class="col">
<strong>Your Order Reference</strong>
<div t-field="o.partner_ref"/>
</div>
<div t-if="o.state in ['purchase','done'] and o.date_approve" class="col-3 bm-2">
<div t-if="o.state == 'purchase' and o.date_approve" class="col-3 bm-2">
<strong>Order Date:</strong>
<p t-field="o.date_approve" class="m-0"/>
<p t-field="o.date_approve" t-options="{'date_only': 'true'}" class="m-0"/>
</div>
<div t-elif="o.date_order" class="col-3 bm-2">
<strong >Order Deadline:</strong>
<p t-field="o.date_order" class="m-0"/>
<div t-elif="o.date_order" class="col-2 bm-2">
<strong>Order Deadline:</strong>
<p t-field="o.date_order" t-options="{'date_only': 'true'}" class="m-0"/>
</div>
<div t-if="o.date_planned" class="col-2 bm-2">
<strong>Expected Arrival:</strong>
<p t-field="o.date_planned" t-options="{'date_only': 'true'}" class="m-0"/>
</div>
</div>
<table class="table table-sm o_main_table table-borderless mt-4">
<table class="o_has_total_table table o_main_table table-borderless">
<thead style="display: table-row-group">
<tr>
<th name="th_description"><strong>Description</strong></th>
<th name="th_taxes"><strong>Taxes</strong></th>
<th name="th_date_req" class="text-center"><strong>Date Req.</strong></th>
<th name="th_description" class="text-start"><strong>Description</strong></th>
<th name="th_quantity" class="text-end"><strong>Qty</strong></th>
<th name="th_price_unit" class="text-end"><strong>Unit Price</strong></th>
<th name="th_amount" class="text-end"><strong>Amount</strong></th>
<th name="th_discount" class="text-end"><strong>Disc.</strong></th>
<th name="th_taxes" class="text-end"><strong>Taxes</strong></th>
<th name="th_subtotal" class="text-end">
<strong>Amount</strong>
</th>
</tr>
</thead>
<tbody>
<t t-set="current_subtotal" t-value="0"/>
<t t-foreach="o.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"/>
<t t-foreach="o.order_line.filtered(lambda l: l.display_type or l.product_qty != 0)" t-as="line">
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal"/>
<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 ''">
<tr t-att-class="'fw-bolder o_line_section' if line.display_type == 'line_section' else 'fw-bold o_line_subsection' if line.display_type == 'line_subsection' else 'fst-italic o_line_note' if line.display_type == 'line_note' else ''">
<t t-if="not line.display_type">
<td id="product">
<td id="product" class="text-start">
<span t-field="line.name"/>
</td>
<t t-set="taxes" t-value="', '.join([(tax.description or tax.name) for tax in line.taxes_id])"/>
<td name="td_taxes" t-attf-class="text-end {{ 'text-nowrap' if len(taxes) &lt; 10 else '' }}">
<span t-out="taxes">Tax 15%</span>
</td>
<td class="text-center">
<span t-field="line.date_planned"/>
</td>
<td class="text-end">
<span t-field="line.product_qty"/>
<span t-field="line.product_uom.name" groups="uom.group_uom"/>
<span t-field="line.product_uom_id.name" groups="uom.group_uom"/>
<span t-if="line.product_uom_id != line.product_id.uom_id" class="text-muted small">
<br/>
<span t-field="line.product_uom_qty" t-options="{'widget': 'float', 'decimal_precision': 'Product Unit'}"/> <span t-field="line.product_id.uom_id"/>
</span>
</td>
<td class="text-end">
<span t-field="line.price_unit"/>
</td>
<td class="text-end">
<span class="text-align-bottom"><span t-field="line.discount"/>%</span>
</td>
<td class="text-end">
<span t-out="', '.join(tax.tax_label for tax in line.tax_ids if tax.tax_label)"/>
</td>
<td class="text-end">
<span t-field="line.price_subtotal"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</t>
<t t-if="line.display_type == 'line_section'">
<t t-if="line.display_type in ('line_section', 'line_subsection')">
<td colspan="99" id="section">
<span t-field="line.name"/>
</td>
@ -99,12 +109,12 @@
</td>
</t>
</tr>
<t t-if="current_section and (line_last or o.order_line[line_index+1].display_type == 'line_section')">
<t t-if="current_section and (line_last or o.order_line[line_index+1].display_type in ('line_section', 'line_subsection'))">
<tr class="is-subtotal text-end">
<td colspan="99" id="subtotal">
<strong class="mr16">Subtotal</strong>
<span
t-esc="current_subtotal"
t-out="current_subtotal"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'
/>
</td>
@ -114,17 +124,28 @@
</tbody>
</table>
<div id="total" class="row justify-content-end">
<div id="total" class="row justify-content-end mt-n3">
<div class="col-4">
<table class="table table-sm table-borderless">
<t t-set="tax_totals" t-value="o.tax_totals"/>
<t t-call="account.document_tax_totals"/>
<table class="o_total_table table table-borderless">
<t t-call="purchase.document_tax_totals">
<t t-set="tax_totals" t-value="o.tax_totals"/>
<t t-set="currency" t-value="o.currency_id"/>
</t>
</table>
</div>
</div>
<p t-field="o.notes" class="mt-4"/>
<p t-field="o.note" class="mt-4"/>
<div class="oe_structure"/>
<strong>Payment Terms: </strong>
<span t-field="o.payment_term_id" class="mt-4"></span>
<t t-set="base_address" t-value="o.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="portal_url" t-value="base_address + '/my/purchase/' + str(o.id) + '#portal_connect_software_modal'"/>
<div t-if="any(u._is_portal() for u in o.partner_id.user_ids) and o._get_edi_builders()" class="text-center">
<a t-att-href="portal_url">Connect your software</a> with <t t-out="o.company_id.name"/> to create quotes automatically.
</div>
</div>
</t>
</template>
@ -136,4 +157,8 @@
</t>
</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>
</odoo>

View file

@ -10,17 +10,22 @@
</t>
<t t-if="o.dest_address_id">
<t t-set="information_block">
<strong>Shipping address:</strong>
<strong class="d-block mt-3">Shipping address</strong>
<div t-field="o.dest_address_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}' name="purchase_shipping_address"/>
<div>
<strong>Requested Ship Date:</strong>
<span t-field="o.date_planned" t-options="{'date_only': 'true'}"/>
</div>
</t>
</t>
<div class="page">
<div class="oe_structure"/>
<t t-set="layout_document_title">
<span>Request for Quotation <span t-field="o.name"/></span>
</t>
<h2 class="mt-4">Request for Quotation <span t-field="o.name"/></h2>
<table class="table table-sm mt-4">
<table class="table table-borderless">
<thead style="display: table-row-group">
<tr>
<th name="th_description"><strong>Description</strong></th>
@ -29,18 +34,22 @@
</tr>
</thead>
<tbody>
<t t-foreach="o.order_line" t-as="order_line">
<tr t-att-class="'bg-200 fw-bold o_line_section' if order_line.display_type == 'line_section' else 'fst-italic o_line_note' if order_line.display_type == 'line_note' else ''">
<t t-foreach="o.order_line.filtered(lambda l: l.display_type or l.product_qty != 0)" t-as="order_line">
<tr t-att-class="'fw-bolder o_line_section' if order_line.display_type == 'line_section' else 'fw-bold o_line_subsection' if order_line.display_type == 'line_subsection' else 'fst-italic o_line_note' if order_line.display_type == 'line_note' else ''">
<t t-if="not order_line.display_type">
<td id="product">
<span t-field="order_line.name"/>
</td>
<td class="text-center">
<span t-field="order_line.date_planned"/>
<span t-field="order_line.date_planned" t-options="{'date_only': 'true'}"/>
</td>
<td class="text-end">
<span t-field="order_line.product_qty"/>
<span t-field="order_line.product_uom" groups="uom.group_uom"/>
<span t-field="order_line.product_uom_id" groups="uom.group_uom"/>
<span t-if="order_line.product_uom_id != order_line.product_id.uom_id" class="text-muted small">
<br/>
<span t-field="order_line.product_uom_qty" t-options="{'widget': 'float', 'decimal_precision': 'Product Unit'}"/> <span t-field="order_line.product_id.uom_id"/>
</span>
</td>
</t>
<t t-else="">
@ -53,9 +62,15 @@
</tbody>
</table>
<p t-field="o.notes" class="mt-4"/>
<p t-field="o.note" class="mt-4"/>
<div class="oe_structure"/>
<t t-set="base_address" t-value="o.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="portal_url" t-value="base_address + '/my/purchase/' + str(o.id) + '#portal_connect_software_modal'"/>
<div t-if="any(u._is_portal() for u in o.partner_id.user_ids) and o._get_edi_builders()" class="text-center">
<a t-att-href="portal_url">Connect your software</a> with <t t-out="o.company_id.name"/> to create quotes automatically.
</div>
</div>
</t>
</template>

View file

@ -5,15 +5,13 @@
# Please note that these reports are not multi-currency !!!
#
import re
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv.expression import AND, expression
from odoo import fields, models, api
from odoo.tools.query import Query
from odoo.tools.sql import SQL
class PurchaseReport(models.Model):
_name = "purchase.report"
_name = 'purchase.report'
_description = "Purchase Report"
_auto = False
_order = 'date_order desc, price_total desc'
@ -24,25 +22,20 @@ class PurchaseReport(models.Model):
('sent', 'RFQ Sent'),
('to approve', 'To Approve'),
('purchase', 'Purchase Order'),
('done', 'Done'),
('cancel', 'Cancelled')
], 'Status', readonly=True)
product_id = fields.Many2one('product.product', 'Product', readonly=True)
partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True)
date_approve = fields.Datetime('Confirmation Date', readonly=True)
product_uom = fields.Many2one('uom.uom', 'Reference Unit of Measure', required=True)
product_uom_id = fields.Many2one('uom.uom', 'Reference Unit of Measure', readonly=True)
company_id = fields.Many2one('res.company', 'Company', readonly=True)
currency_id = fields.Many2one('res.currency', 'Currency', readonly=True)
user_id = fields.Many2one('res.users', 'Purchase Representative', readonly=True)
delay = fields.Float('Days to Confirm', digits=(16, 2), readonly=True, group_operator='avg', help="Amount of time between purchase approval and order by date.")
delay_pass = fields.Float('Days to Receive', digits=(16, 2), readonly=True, group_operator='avg',
user_id = fields.Many2one('res.users', 'Buyer', readonly=True)
delay = fields.Float('Days to Confirm', digits=(16, 2), readonly=True, aggregator='avg', help="Amount of time between purchase approval and order by date.")
delay_pass = fields.Float('Days to Receive', digits=(16, 2), readonly=True, aggregator='avg',
help="Amount of time between date planned and order by date for each purchase order line.")
avg_days_to_purchase = fields.Float(
'Average Days to Purchase', digits=(16, 2), readonly=True, store=False, # needs store=False to prevent showing up as a 'measure' option
help="Amount of time between purchase approval and document creation date. Due to a hack needed to calculate this, \
every record will show the same average value, therefore only use this as an aggregated value with group_operator=avg")
price_total = fields.Float('Total', readonly=True)
price_average = fields.Float('Average Cost', readonly=True, group_operator="avg", digits='Product Price')
price_total = fields.Monetary('Total', readonly=True)
price_average = fields.Monetary('Average Cost', readonly=True, aggregator="avg")
nbr_lines = fields.Integer('# of Lines', readonly=True)
category_id = fields.Many2one('product.category', 'Product Category', readonly=True)
product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
@ -52,19 +45,20 @@ class PurchaseReport(models.Model):
weight = fields.Float('Gross Weight', readonly=True)
volume = fields.Float('Volume', readonly=True)
order_id = fields.Many2one('purchase.order', 'Order', readonly=True)
untaxed_total = fields.Float('Untaxed Total', readonly=True)
untaxed_total = fields.Monetary('Untaxed Total', readonly=True)
qty_ordered = fields.Float('Qty Ordered', readonly=True)
qty_received = fields.Float('Qty Received', readonly=True)
qty_billed = fields.Float('Qty Billed', readonly=True)
qty_to_be_billed = fields.Float('Qty to be Billed', readonly=True)
@property
def _table_query(self):
def _table_query(self) -> SQL:
''' Report needs to be dynamic to take into account multi-company selected + multi-currency rates '''
return '%s %s %s %s' % (self._select(), self._from(), self._where(), self._group_by())
return SQL("%s %s %s %s", self._select(), self._from(), self._where(), self._group_by())
def _select(self):
select_str = """
def _select(self) -> SQL:
return SQL(
"""
SELECT
po.id as order_id,
min(l.id) as id,
@ -80,29 +74,30 @@ class PurchaseReport(models.Model):
p.product_tmpl_id,
t.categ_id as category_id,
c.currency_id,
t.uom_id as product_uom,
t.uom_id as product_uom_id,
extract(epoch from age(po.date_approve,po.date_order))/(24*60*60)::decimal(16,2) as delay,
extract(epoch from age(l.date_planned,po.date_order))/(24*60*60)::decimal(16,2) as delay_pass,
count(*) as nbr_lines,
sum(l.price_total / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as price_total,
(sum(l.product_qty * l.price_unit / COALESCE(po.currency_rate, 1.0))/NULLIF(sum(l.product_qty/line_uom.factor*product_uom.factor),0.0))::decimal(16,2) * currency_table.rate as price_average,
sum(l.price_total / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * account_currency_table.rate as price_total,
(sum(l.product_qty * l.price_unit / COALESCE(po.currency_rate, 1.0))/NULLIF(sum(l.product_qty * line_uom.factor / product_uom.factor),0.0))::decimal(16,2) * account_currency_table.rate as price_average,
partner.country_id as country_id,
partner.commercial_partner_id as commercial_partner_id,
sum(p.weight * l.product_qty/line_uom.factor*product_uom.factor) as weight,
sum(p.volume * l.product_qty/line_uom.factor*product_uom.factor) as volume,
sum(l.price_subtotal / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as untaxed_total,
sum(l.product_qty / line_uom.factor * product_uom.factor) as qty_ordered,
sum(l.qty_received / line_uom.factor * product_uom.factor) as qty_received,
sum(l.qty_invoiced / line_uom.factor * product_uom.factor) as qty_billed,
case when t.purchase_method = 'purchase'
then sum(l.product_qty / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor)
else sum(l.qty_received / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor)
sum(p.weight * l.product_qty * line_uom.factor / product_uom.factor) as weight,
sum(p.volume * l.product_qty * line_uom.factor / product_uom.factor) as volume,
sum(l.price_subtotal / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * account_currency_table.rate as untaxed_total,
sum(l.product_qty * line_uom.factor / product_uom.factor) as qty_ordered,
sum(l.qty_received * line_uom.factor / product_uom.factor) as qty_received,
sum(l.qty_invoiced * line_uom.factor / product_uom.factor) as qty_billed,
case when t.purchase_method = 'purchase'
then sum(l.product_qty * line_uom.factor / product_uom.factor) - sum(l.qty_invoiced * line_uom.factor / product_uom.factor)
else sum(l.qty_received * line_uom.factor / product_uom.factor) - sum(l.qty_invoiced * line_uom.factor / product_uom.factor)
end as qty_to_be_billed
"""
return select_str
""",
)
def _from(self):
from_str = """
def _from(self) -> SQL:
return SQL(
"""
FROM
purchase_order_line l
join purchase_order po on (l.order_id=po.id)
@ -110,22 +105,24 @@ class PurchaseReport(models.Model):
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 res_company C ON C.id = po.company_id
left join uom_uom line_uom on (line_uom.id=l.product_uom)
left join uom_uom line_uom on (line_uom.id=l.product_uom_id)
left join uom_uom product_uom on (product_uom.id=t.uom_id)
left join {currency_table} ON currency_table.company_id = po.company_id
""".format(
currency_table=self.env['res.currency']._get_query_currency_table({'multi_company': True, 'date': {'date_to': fields.Date.today()}}),
left join %(currency_table)s ON account_currency_table.company_id = po.company_id
""",
currency_table=self.env['res.currency']._get_simple_currency_table(self.env.companies),
)
return from_str
def _where(self):
return """
def _where(self) -> SQL:
return SQL(
"""
WHERE
l.display_type IS NULL
"""
""",
)
def _group_by(self):
group_by_str = """
def _group_by(self) -> SQL:
return SQL(
"""
GROUP BY
po.company_id,
po.user_id,
@ -135,7 +132,7 @@ class PurchaseReport(models.Model):
l.price_unit,
po.date_approve,
l.date_planned,
l.product_uom,
l.product_uom_id,
po.dest_address_id,
po.fiscal_position_id,
l.product_id,
@ -143,8 +140,6 @@ class PurchaseReport(models.Model):
t.categ_id,
po.date_order,
po.state,
line_uom.uom_type,
line_uom.category_id,
t.uom_id,
t.purchase_method,
line_uom.id,
@ -152,63 +147,16 @@ class PurchaseReport(models.Model):
partner.country_id,
partner.commercial_partner_id,
po.id,
currency_table.rate
"""
return group_by_str
account_currency_table.rate
""",
)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
""" This is a hack to allow us to correctly calculate the average of PO specific date values since
the normal report query result will duplicate PO values across its PO lines during joins and
lead to incorrect aggregation values.
Only the AVG operator is supported for avg_days_to_purchase.
"""
avg_days_to_purchase = next((field for field in fields if re.search(r'\bavg_days_to_purchase\b', field)), False)
if avg_days_to_purchase:
fields.remove(avg_days_to_purchase)
if any(field.split(':')[1].split('(')[0] != 'avg' for field in [avg_days_to_purchase] if field):
raise UserError(_("Value: 'avg_days_to_purchase' should only be used to show an average. If you are seeing this message then it is being accessed incorrectly."))
if 'price_average:avg' in fields:
fields.extend(['aggregated_qty_ordered:array_agg(qty_ordered)'])
fields.extend(['aggregated_price_average:array_agg(price_average)'])
res = []
if fields:
res = super(PurchaseReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
if 'price_average:avg' in fields:
qties = 'aggregated_qty_ordered'
special_field = 'aggregated_price_average'
for data in res:
if data[special_field] and data[qties]:
total_unit_cost = sum(float(value) * float(qty) for value, qty in zip(data[special_field], data[qties]) if qty and value)
total_qty_ordered = sum(float(qty) for qty in data[qties] if qty)
data['price_average'] = (total_unit_cost / total_qty_ordered) if total_qty_ordered else 0
del data[special_field]
del data[qties]
if not res and avg_days_to_purchase:
res = [{}]
if avg_days_to_purchase:
self.check_access_rights('read')
query = """ SELECT AVG(days_to_purchase.po_days_to_purchase)::decimal(16,2) AS avg_days_to_purchase
FROM (
SELECT extract(epoch from age(po.date_approve,po.create_date))/(24*60*60) AS po_days_to_purchase
FROM purchase_order po
WHERE po.id IN (
SELECT "purchase_report"."order_id" FROM %s WHERE %s)
) AS days_to_purchase
"""
subdomain = AND([domain, [('company_id', '=', self.env.company.id), ('date_approve', '!=', False)]])
subtables, subwhere, subparams = expression(subdomain, self).query.get_sql()
self.env.cr.execute(query % (subtables, subwhere), subparams)
res[0].update({
'__count': 1,
avg_days_to_purchase.split(':')[0]: self.env.cr.fetchall()[0][0],
})
return res
def _read_group_select(self, aggregate_spec: str, query: Query) -> SQL:
""" This override allows us to correctly calculate the average price of products. """
if aggregate_spec != 'price_average:avg':
return super()._read_group_select(aggregate_spec, query)
return SQL(
'SUM(%(f_price)s * %(f_qty)s) / NULLIF(SUM(%(f_qty)s), 0.0)',
f_qty=self._field_to_sql(self._table, 'qty_ordered', query),
f_price=self._field_to_sql(self._table, 'price_average', query),
)

View file

@ -6,7 +6,7 @@
<field name="arch" type="xml">
<pivot string="Purchase Analysis" display_quantity="1" sample="1">
<field name="category_id" type="row"/>
<field name="order_id" type="measure"/>
<field name="order_id" type="row"/>
<field name="untaxed_total" type="measure"/>
<field name="price_total" type="measure"/>
</pivot>
@ -24,11 +24,11 @@
</record>
<record id="purchase_report_view_tree" model="ir.ui.view">
<field name="name">purchase.report.view.tree</field>
<field name="name">purchase.report.view.list</field>
<field name="model">purchase.report</field>
<field name="arch" type="xml">
<tree string="Purchase Analysis">
<field name="date_order" widget="date"/>
<list string="Purchase Analysis">
<field name="date_order"/>
<field name="order_id" optional="show"/>
<field name="partner_id" optional="show"/>
<field name="product_id" optional="show"/>
@ -38,11 +38,11 @@
<field name="qty_ordered" optional="hide" sum="Sum of Qty Ordered"/>
<field name="qty_received" optional="hide" sum="Sum of Qty Received"/>
<field name="qty_billed" optional="hide" sum="Sum of Qty Billed"/>
<field name="currency_id" optional="show" invisible="1"/>
<field name="currency_id" optional="show" column_invisible="True"/>
<field name="untaxed_total" optional="hide" widget="monetary" sum="Sum of Untaxed Total"/>
<field name="price_total" optional="show" widget="monetary" sum="Sum of Total"/>
<field name="state" optional="show"/>
</tree>
</list>
</field>
</record>
@ -53,22 +53,22 @@
<search string="Purchase Orders">
<filter string="Requests for Quotation" name="quotes" domain="[('state','in',('draft','sent'))]"/>
<filter string="Purchase Orders" name="orders" domain="[('state','!=','draft'), ('state','!=','sent'), ('state','!=','cancel')]"/>
<filter string="Confirmation Date Last Year" name="later_than_a_year_ago" domain="[('date_approve', '&gt;=', ((context_today()-relativedelta(years=1)).strftime('%Y-%m-%d')))]"/>
<filter string="Confirmation Date Last Year" name="later_than_a_year_ago" domain="[('date_approve', '&gt;=', 'today -1y')]"/>
<filter name="filter_date_order" date="date_order"/>
<filter name="filter_date_approve" date="date_approve" default_period="this_month"/>
<filter name="filter_date_approve" date="date_approve" default_period="month"/>
<field name="partner_id"/>
<field name="product_id"/>
<group expand="0" string="Extended Filters">
<group>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="date_order"/>
<field name="date_approve"/>
<field name="category_id" filter_domain="[('category_id', 'child_of', self)]"/>
</group>
<group expand="1" string="Group By">
<group>
<filter string="Vendor" name="group_partner_id" context="{'group_by':'partner_id'}"/>
<filter string="Vendor Country" name="country_id" context="{'group_by':'country_id'}"/>
<filter string="Purchase Representative" name="user_id" context="{'group_by':'user_id'}"/>
<filter string="Buyer" name="user_id" context="{'group_by':'user_id'}"/>
<filter string="Product" name="group_product_id" context="{'group_by':'product_id'}"/>
<filter string="Product Category" name="group_category_id" context="{'group_by':'category_id'}"/>
<filter string="Status" name="status" context="{'group_by':'state'}"/>
@ -84,8 +84,12 @@
<record id="action_purchase_order_report_all" model="ir.actions.act_window">
<field name="name">Purchase Analysis</field>
<field name="res_model">purchase.report</field>
<field name="path">purchase-analysis</field>
<field name="view_mode">graph,pivot</field>
<field name="view_id"></field> <!-- force empty -->
<field name="context">{
'search_default_orders': 1,
'search_default_filter_date_approve': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No Purchase Analysis