summaryrefslogtreecommitdiff
path: root/addons/purchase_stock/report
diff options
context:
space:
mode:
Diffstat (limited to 'addons/purchase_stock/report')
-rw-r--r--addons/purchase_stock/report/__init__.py7
-rw-r--r--addons/purchase_stock/report/purchase_report.py70
-rw-r--r--addons/purchase_stock/report/purchase_report_templates.xml46
-rw-r--r--addons/purchase_stock/report/purchase_report_views.xml15
-rw-r--r--addons/purchase_stock/report/report_stock_forecasted.py32
-rw-r--r--addons/purchase_stock/report/report_stock_forecasted.xml12
-rw-r--r--addons/purchase_stock/report/report_stock_rule.py17
-rw-r--r--addons/purchase_stock/report/report_stock_rule.xml32
-rw-r--r--addons/purchase_stock/report/vendor_delay_report.py87
-rw-r--r--addons/purchase_stock/report/vendor_delay_report.xml35
10 files changed, 353 insertions, 0 deletions
diff --git a/addons/purchase_stock/report/__init__.py b/addons/purchase_stock/report/__init__.py
new file mode 100644
index 00000000..68360661
--- /dev/null
+++ b/addons/purchase_stock/report/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import purchase_report
+from . import report_stock_forecasted
+from . import report_stock_rule
+from . import vendor_delay_report
diff --git a/addons/purchase_stock/report/purchase_report.py b/addons/purchase_stock/report/purchase_report.py
new file mode 100644
index 00000000..67e63816
--- /dev/null
+++ b/addons/purchase_stock/report/purchase_report.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import re
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+from odoo.osv.expression import expression
+
+
+class PurchaseReport(models.Model):
+ _inherit = "purchase.report"
+
+ picking_type_id = fields.Many2one('stock.warehouse', 'Warehouse', readonly=True)
+ avg_receipt_delay = fields.Float(
+ 'Average Receipt Delay', digits=(16, 2), readonly=True, store=False, # needs store=False to prevent showing up as a 'measure' option
+ help="Amount of time between expected and effective receipt 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")
+ effective_date = fields.Datetime(string="Effective Date")
+
+ def _select(self):
+ return super(PurchaseReport, self)._select() + ", spt.warehouse_id as picking_type_id, po.effective_date as effective_date"
+
+ def _from(self):
+ return super(PurchaseReport, self)._from() + " left join stock_picking_type spt on (spt.id=po.picking_type_id)"
+
+ def _group_by(self):
+ return super(PurchaseReport, self)._group_by() + ", spt.warehouse_id, effective_date"
+
+ @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_receipt_delay.
+ """
+ avg_receipt_delay = next((field for field in fields if re.search(r'\bavg_receipt_delay\b', field)), False)
+
+ if avg_receipt_delay:
+ fields.remove(avg_receipt_delay)
+ if any(field.split(':')[1].split('(')[0] != 'avg' for field in [avg_receipt_delay] if field):
+ raise UserError("Value: 'avg_receipt_delay' should only be used to show an average. If you are seeing this message then it is being accessed incorrectly.")
+
+ res = []
+ if fields:
+ res = super(PurchaseReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
+
+ if not res and avg_receipt_delay:
+ res = [{}]
+
+ if avg_receipt_delay:
+ query = """ SELECT AVG(receipt_delay.po_receipt_delay)::decimal(16,2) AS avg_receipt_delay
+ FROM (
+ SELECT extract(epoch from age(po.effective_date, po.date_planned))/(24*60*60) AS po_receipt_delay
+ FROM purchase_order po
+ WHERE po.id IN (
+ SELECT "purchase_report"."order_id" FROM %s WHERE %s)
+ ) AS receipt_delay
+ """
+
+ subdomain = domain + [('company_id', '=', self.env.company.id), ('effective_date', '!=', False)]
+ subtables, subwhere, subparams = expression(subdomain, self).query.get_sql()
+
+ self.env.cr.execute(query % (subtables, subwhere), subparams)
+ res[0].update({
+ '__count': 1,
+ avg_receipt_delay.split(':')[0]: self.env.cr.fetchall()[0][0],
+ })
+ return res
diff --git a/addons/purchase_stock/report/purchase_report_templates.xml b/addons/purchase_stock/report/purchase_report_templates.xml
new file mode 100644
index 00000000..89acd21a
--- /dev/null
+++ b/addons/purchase_stock/report/purchase_report_templates.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <template id="report_purchaseorder_document" inherit_id="purchase.report_purchaseorder_document">
+ <xpath expr="//t[@t-if='o.dest_address_id']" position="after">
+ <t t-else="">
+ <t t-set="information_block">
+ <strong>Shipping address:</strong>
+ <div t-if="o.picking_type_id and o.picking_type_id.warehouse_id">
+ <span t-field="o.picking_type_id.warehouse_id.name"/>
+ <div t-field="o.picking_type_id.warehouse_id.partner_id" t-options='{"widget": "contact", "fields": ["address", "phone"], "no_marker": True, "phone_icons": True}'/>
+ </div>
+ </t>
+ </t>
+ </xpath>
+ <xpath expr="//div[@t-if='o.date_order']" position="after">
+ <div t-if="o.incoterm_id" class="col-3 bm-2">
+ <strong>Incoterm:</strong>
+ <p t-field="o.incoterm_id.code" class="m-0"/>
+ </div>
+ </xpath>
+ </template>
+
+ <template id="report_purchasequotation_document" inherit_id="purchase.report_purchasequotation_document">
+ <xpath expr="//t[@t-if='o.dest_address_id']" position="after">
+ <t t-else="">
+ <t t-set="information_block">
+ <strong>Shipping address:</strong>
+ <div t-if="o.picking_type_id and o.picking_type_id.warehouse_id">
+ <span t-field="o.picking_type_id.warehouse_id.name"/>
+ <div t-field="o.picking_type_id.warehouse_id.partner_id" t-options='{"widget": "contact", "fields": ["address", "phone"], "no_marker": True, "phone_icons": True}'/>
+ </div>
+ </t>
+ </t>
+ </xpath>
+ <xpath expr="//span[@t-field='o.name']/.." position="after">
+ <div id="informations" class="row mt16 mb16">
+ <div t-if="o.incoterm_id" class="col-3 bm-2">
+ <strong>Incoterm:</strong>
+ <p t-field="o.incoterm_id.code" class="m-0"/>
+ </div>
+ </div>
+ </xpath>
+ </template>
+
+</odoo>
diff --git a/addons/purchase_stock/report/purchase_report_views.xml b/addons/purchase_stock/report/purchase_report_views.xml
new file mode 100644
index 00000000..ad272a7b
--- /dev/null
+++ b/addons/purchase_stock/report/purchase_report_views.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+ <record id="purchase_report_view_search" model="ir.ui.view">
+ <field name="name">purchase.report.search.stock</field>
+ <field name="model">purchase.report</field>
+ <field name="inherit_id" ref="purchase.view_purchase_order_search"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='user_id']" position="after">
+ <field name="picking_type_id"/>
+ </xpath>
+ </field>
+ </record>
+
+</odoo>
diff --git a/addons/purchase_stock/report/report_stock_forecasted.py b/addons/purchase_stock/report/report_stock_forecasted.py
new file mode 100644
index 00000000..6ae5459a
--- /dev/null
+++ b/addons/purchase_stock/report/report_stock_forecasted.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class ReplenishmentReport(models.AbstractModel):
+ _inherit = 'report.stock.report_product_product_replenishment'
+
+ def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids):
+ res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids)
+ domain = [('state', 'in', ['draft', 'sent'])]
+ domain += self._product_purchase_domain(product_template_ids, product_variant_ids)
+ warehouse_id = self.env.context.get('warehouse', False)
+ if warehouse_id:
+ domain += [('order_id.picking_type_id.warehouse_id', '=', warehouse_id)]
+ po_lines = self.env['purchase.order.line'].read_group(domain, ['product_uom_qty'], 'product_id')
+ in_sum = sum(line['product_uom_qty'] for line in po_lines)
+
+ res['draft_purchase_qty'] = in_sum
+ res['qty']['in'] += in_sum
+ return res
+
+ def _product_purchase_domain(self, product_template_ids, product_variant_ids):
+ if product_variant_ids:
+ return [('product_id', 'in', product_variant_ids)]
+ elif product_template_ids:
+ products = self.env['product.product'].search_read(
+ [('product_tmpl_id', 'in', product_template_ids)], ['id']
+ )
+ product_ids = [product['id'] for product in products]
+ return [('product_id', 'in', product_ids)]
diff --git a/addons/purchase_stock/report/report_stock_forecasted.xml b/addons/purchase_stock/report/report_stock_forecasted.xml
new file mode 100644
index 00000000..65538563
--- /dev/null
+++ b/addons/purchase_stock/report/report_stock_forecasted.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="purchase_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment">
+ <xpath expr="//tr[@name='draft_picking_in']" position="after">
+ <tr t-if="docs['draft_purchase_qty']" name="draft_po_in">
+ <td colspan="2">Draft PO</td>
+ <td t-esc="docs['draft_purchase_qty']" class="text-right"/>
+ <td t-esc="docs['uom']" groups="uom.group_uom"/>
+ </tr>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/purchase_stock/report/report_stock_rule.py b/addons/purchase_stock/report/report_stock_rule.py
new file mode 100644
index 00000000..cc92df7c
--- /dev/null
+++ b/addons/purchase_stock/report/report_stock_rule.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class ReportStockRule(models.AbstractModel):
+ _inherit = 'report.stock.report_stock_rule'
+
+ @api.model
+ def _get_rule_loc(self, rule, product_id):
+ """ We override this method to handle buy rules which do not have a location_src_id.
+ """
+ res = super(ReportStockRule, self)._get_rule_loc(rule, product_id)
+ if rule.action == 'buy':
+ res['source'] = self.env.ref('stock.stock_location_suppliers')
+ return res
diff --git a/addons/purchase_stock/report/report_stock_rule.xml b/addons/purchase_stock/report/report_stock_rule.xml
new file mode 100644
index 00000000..7d524099
--- /dev/null
+++ b/addons/purchase_stock/report/report_stock_rule.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <template id="purchase_report_stock_rule" inherit_id="stock.report_stock_rule">
+ <xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t" position="before">
+ <t t-if="rule[0].action == 'buy'">
+ <t t-if="rule[1] == 'origin'">
+ <t t-call="stock.report_stock_rule_left_arrow"/>
+ </t>
+ </t>
+ </xpath>
+ <xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t[last()]" position="after">
+ <t t-if="rule[0].action == 'buy'">
+ <t t-if="rule[1] == 'destination'">
+ <t t-call="stock.report_stock_rule_right_arrow"/>
+ </t>
+ </t>
+ </xpath>
+ <xpath expr="//div[hasclass('o_report_stock_rule_rule_name')]/span" position="before">
+ <t t-if="rule[0].action == 'buy'">
+ <i class="fa fa-shopping-cart fa-fw" t-attf-style="color: #{color};"/>
+ </t>
+ </xpath>
+ <xpath expr="//div[hasclass('o_report_stock_rule_legend')]" position="inside">
+ <div class="o_report_stock_rule_legend_line">
+ <div class="o_report_stock_rule_legend_label">Buy</div>
+ <div class="o_report_stock_rule_legend_symbol">
+ <div class="fa fa-shopping-cart fa-fw" t-attf-style="color: #{color};"/>
+ </div>
+ </div>
+ </xpath>
+ </template>
+</odoo> \ No newline at end of file
diff --git a/addons/purchase_stock/report/vendor_delay_report.py b/addons/purchase_stock/report/vendor_delay_report.py
new file mode 100644
index 00000000..ef55b2ca
--- /dev/null
+++ b/addons/purchase_stock/report/vendor_delay_report.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
+from odoo.exceptions import UserError
+from odoo.osv.expression import expression
+
+
+class VendorDelayReport(models.Model):
+ _name = "vendor.delay.report"
+ _description = "Vendor Delay Report"
+ _auto = False
+
+ partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True)
+ product_id = fields.Many2one('product.product', 'Product', readonly=True)
+ category_id = fields.Many2one('product.category', 'Product Category', readonly=True)
+ date = fields.Datetime('Effective Date', readonly=True)
+ qty_total = fields.Float('Total Quantity', readonly=True)
+ qty_on_time = fields.Float('On-Time Quantity', readonly=True)
+ on_time_rate = fields.Float('On-Time Delivery Rate', readonly=True)
+
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, 'vendor_delay_report')
+ self.env.cr.execute("""
+CREATE OR replace VIEW vendor_delay_report AS(
+SELECT m.id AS id,
+ m.date AS date,
+ m.purchase_line_id AS purchase_line_id,
+ m.product_id AS product_id,
+ Min(pc.id) AS category_id,
+ Min(po.partner_id) AS partner_id,
+ Sum(pol.product_uom_qty) AS qty_total,
+ Sum(CASE
+ WHEN (pol.date_planned::date >= m.date::date) THEN ml.qty_done
+ ELSE 0
+ END) AS qty_on_time
+FROM stock_move m
+ JOIN stock_move_line ml
+ ON m.id = ml.move_id
+ JOIN purchase_order_line pol
+ ON pol.id = m.purchase_line_id
+ JOIN purchase_order po
+ ON po.id = pol.order_id
+ JOIN product_product p
+ ON p.id = m.product_id
+ JOIN product_template pt
+ ON pt.id = p.product_tmpl_id
+ JOIN product_category pc
+ ON pc.id = pt.categ_id
+WHERE m.state = 'done'
+GROUP BY m.id
+)""")
+
+ @api.model
+ def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+ if all('on_time_rate' not in field for field in fields):
+ res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
+ return res
+
+ for field in fields:
+ if 'on_time_rate' not in field:
+ continue
+
+ fields.remove(field)
+
+ agg = field.split(':')[1:]
+ if agg and agg[0] != 'sum':
+ raise NotImplementedError('Aggregate functions other than \':sum\' are not allowed.')
+
+ qty_total = field.replace('on_time_rate', 'qty_total')
+ if qty_total not in fields:
+ fields.append(qty_total)
+ qty_on_time = field.replace('on_time_rate', 'qty_on_time')
+ if qty_on_time not in fields:
+ fields.append(qty_on_time)
+ break
+
+ res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
+
+ for group in res:
+ if group['qty_total'] == 0:
+ on_time_rate = 100
+ else:
+ on_time_rate = group['qty_on_time'] / group['qty_total'] * 100
+ group.update({'on_time_rate': on_time_rate})
+
+ return res
diff --git a/addons/purchase_stock/report/vendor_delay_report.xml b/addons/purchase_stock/report/vendor_delay_report.xml
new file mode 100644
index 00000000..cd4601cc
--- /dev/null
+++ b/addons/purchase_stock/report/vendor_delay_report.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="vendor_delay_report_filter" model="ir.ui.view">
+ <field name="name">vendor.delay.report.search</field>
+ <field name="model">vendor.delay.report</field>
+ <field name="arch" type="xml">
+ <search string="On-time Delivery">
+ <field name="partner_id"/>
+ <field name="product_id"/>
+ <filter string="Effective Date Last Year" name="later_than_a_year_ago" domain="[('date', '&gt;=', ((context_today()-relativedelta(years=1)).strftime('%Y-%m-%d')))]"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="vendor_delay_report_view_graph" model="ir.ui.view">
+ <field name="name">vendor.delay.report.view.graph</field>
+ <field name="model">vendor.delay.report</field>
+ <field name="arch" type="xml">
+ <graph string="On-Time Delivery" type="bar" sample="1" disable_linking="1">
+ <field name="product_id" type="row"/>
+ <field name="on_time_rate" type="measure"/>
+ </graph>
+ </field>
+ </record>
+
+ <record id="action_purchase_vendor_delay_report" model="ir.actions.act_window">
+ <field name="name">On-time Delivery</field>
+ <field name="res_model">vendor.delay.report</field>
+ <field name="view_mode">graph</field>
+ <field name="search_view_id" ref="vendor_delay_report_filter"/>
+ <field name="help">Vendor On-time Delivery analysis</field>
+ <field name="target">current</field>
+ <field name="context">{'search_default_later_than_a_year_ago':1}</field>
+ </record>
+</odoo>