diff options
Diffstat (limited to 'addons/purchase_stock/report')
| -rw-r--r-- | addons/purchase_stock/report/__init__.py | 7 | ||||
| -rw-r--r-- | addons/purchase_stock/report/purchase_report.py | 70 | ||||
| -rw-r--r-- | addons/purchase_stock/report/purchase_report_templates.xml | 46 | ||||
| -rw-r--r-- | addons/purchase_stock/report/purchase_report_views.xml | 15 | ||||
| -rw-r--r-- | addons/purchase_stock/report/report_stock_forecasted.py | 32 | ||||
| -rw-r--r-- | addons/purchase_stock/report/report_stock_forecasted.xml | 12 | ||||
| -rw-r--r-- | addons/purchase_stock/report/report_stock_rule.py | 17 | ||||
| -rw-r--r-- | addons/purchase_stock/report/report_stock_rule.xml | 32 | ||||
| -rw-r--r-- | addons/purchase_stock/report/vendor_delay_report.py | 87 | ||||
| -rw-r--r-- | addons/purchase_stock/report/vendor_delay_report.xml | 35 |
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', '>=', ((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> |
