diff --git a/addons/purchase/data/purchase_demo.xml b/addons/purchase/data/purchase_demo.xml index 43d840bbaeb..2283fd28778 100644 --- a/addons/purchase/data/purchase_demo.xml +++ b/addons/purchase/data/purchase_demo.xml @@ -19,21 +19,21 @@ 'price_unit': 79.80, 'product_qty': 15.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=3)}), (0, 0, { 'product_id': ref('product.product_product_25'), 'name': obj().env.ref('product.product_product_25').partner_ref, 'price_unit': 2868.70, 'product_qty': 5.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=3)}), (0, 0, { 'product_id': ref('product.product_product_27'), 'name': obj().env.ref('product.product_product_27').partner_ref, 'price_unit': 3297.20, 'product_qty': 4.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}) + 'date_planned': DateTime.today() + relativedelta(days=3)}) ]"/> @@ -48,14 +48,14 @@ 'price_unit': 132.50, 'product_qty': 20.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=1)}), (0, 0, { 'product_id': ref('product.product_delivery_01'), 'name': obj().env.ref('product.product_delivery_01').partner_ref, 'price_unit': 89.0, 'product_qty': 5.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=1)}), ]"/> @@ -70,7 +70,7 @@ 'price_unit': 25.50, 'product_qty': 10.0, 'product_uom': ref('uom.product_uom_hour'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=1)}), ]"/> @@ -85,21 +85,21 @@ 'price_unit': 85.50, 'product_qty': 6.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=5)}), (0, 0, { 'product_id': ref('product.product_product_20'), 'name': obj().env.ref('product.product_product_20').partner_ref, 'price_unit': 1690.0, 'product_qty': 5.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today() + relativedelta(days=5)}), (0, 0, { 'product_id': ref('product.product_product_6'), 'name': obj().env.ref('product.product_product_6').partner_ref, 'price_unit': 800.0, 'product_qty': 7.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}) + 'date_planned': DateTime.today() + relativedelta(days=5)}) ]"/> @@ -114,14 +114,14 @@ 'price_unit': 2010.0, 'product_qty': 3.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), (0, 0, { 'product_id': ref('product.product_product_24'), 'name': obj().env.ref('product.product_product_24').partner_ref, 'price_unit': 876.0, 'product_qty': 3.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), ]"/> @@ -136,21 +136,21 @@ 'price_unit': 58.0, 'product_qty': 9.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), (0, 0, { 'product_id': ref('product.product_delivery_01'), 'name': obj().env.ref('product.product_delivery_01').partner_ref, 'price_unit': 65.0, 'product_qty': 3.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), (0, 0, { 'product_id': ref('product.consu_delivery_01'), 'name': obj().env.ref('product.consu_delivery_01').partner_ref, 'price_unit': 154.5, 'product_qty': 4.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), ]"/> @@ -165,14 +165,14 @@ 'price_unit': 13.5, 'product_qty': 5.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), (0, 0, { 'product_id': ref('product.product_delivery_02'), 'name': obj().env.ref('product.product_delivery_02').partner_ref, 'price_unit': 38.0, 'product_qty': 15.0, 'product_uom': ref('uom.product_uom_unit'), - 'date_planned': time.strftime('%Y-%m-%d')}), + 'date_planned': DateTime.today()}), ]"/> diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index fb68ccdec72..a0d1386ffc5 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -85,7 +85,7 @@ class PurchaseOrder(models.Model): "delivery order sent by your vendor.") date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\ help="Depicts the date where the Quotation should be validated and converted into a purchase order.") - date_approve = fields.Date('Confirmation Date', readonly=1, index=True, copy=False) + date_approve = fields.Datetime('Confirmation Date', readonly=1, index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, tracking=True, help="You can find a vendor by its Name, TIN, Email or Internal Reference.") dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES, help="Put an address if you want to deliver directly from the vendor to the customer. " @@ -125,6 +125,7 @@ class PurchaseOrder(models.Model): product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product', readonly=False) user_id = fields.Many2one('res.users', string='Purchase Representative', index=True, tracking=True, default=lambda self: self.env.user) company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.company.id) + currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', compute_sudo=True, store=True, readonly=True, help='Ratio between the purchase order currency and the company currency') def _compute_access_url(self): super(PurchaseOrder, self)._compute_access_url() @@ -140,6 +141,11 @@ class PurchaseOrder(models.Model): purchase_order_ids = self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) return self.browse(purchase_order_ids).name_get() + @api.depends('date_order', 'currency_id', 'company_id', 'company_id.currency_id') + def _compute_currency_rate(self): + for order in self: + order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order) + @api.multi @api.depends('name', 'partner_ref') def name_get(self): diff --git a/addons/purchase/report/purchase_report.py b/addons/purchase/report/purchase_report.py index 28c25cecaca..d3a04e7d06e 100644 --- a/addons/purchase/report/purchase_report.py +++ b/addons/purchase/report/purchase_report.py @@ -25,18 +25,15 @@ class PurchaseReport(models.Model): ], 'Order Status', readonly=True) product_id = fields.Many2one('product.product', 'Product', readonly=True) partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True) - date_approve = fields.Date('Date Approved', readonly=True) + date_approve = fields.Datetime('Confirmation Date', readonly=True) product_uom = fields.Many2one('uom.uom', 'Reference Unit of Measure', required=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 Validate', digits=(16, 2), readonly=True) - delay_pass = fields.Float('Days to Deliver', digits=(16, 2), readonly=True) - unit_quantity = fields.Float('Product Quantity', readonly=True, oldname='quantity') - price_total = fields.Float('Total Price', readonly=True) - price_average = fields.Float('Average Price', readonly=True, group_operator="avg") - negociation = fields.Float('Purchase-Standard Price', readonly=True, group_operator="avg") - price_standard = fields.Float('Products Value', readonly=True, group_operator="sum") + delay = fields.Float('Days to Confirm', digits=(16, 2), readonly=True) + delay_pass = fields.Float('Days to Receive', digits=(16, 2), readonly=True) + price_total = fields.Float('Total', readonly=True) + price_average = fields.Float('Average Cost', readonly=True, group_operator="avg") nbr_lines = fields.Integer('# of Lines', readonly=True, oldname='nbr') category_id = fields.Many2one('product.category', 'Product Category', readonly=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True) @@ -46,6 +43,12 @@ class PurchaseReport(models.Model): commercial_partner_id = fields.Many2one('res.partner', 'Commercial Entity', readonly=True) 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) + 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) @api.model_cr def init(self): @@ -61,80 +64,83 @@ class PurchaseReport(models.Model): select_str = """ WITH currency_rate as (%s) SELECT + po.id as order_id, min(l.id) as id, - s.date_order as date_order, - s.state, - s.date_approve, - s.dest_address_id, - s.partner_id as partner_id, - s.user_id as user_id, - s.company_id as company_id, - s.fiscal_position_id as fiscal_position_id, + po.date_order as date_order, + po.state, + po.date_approve, + po.dest_address_id, + po.partner_id as partner_id, + po.user_id as user_id, + po.company_id as company_id, + po.fiscal_position_id as fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id as category_id, - s.currency_id, + po.currency_id, t.uom_id as product_uom, - sum(l.product_qty/u.factor*u2.factor) as unit_quantity, - extract(epoch from age(s.date_approve,s.date_order))/(24*60*60)::decimal(16,2) as delay, - extract(epoch from age(l.date_planned,s.date_order))/(24*60*60)::decimal(16,2) as delay_pass, + 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_unit / COALESCE(NULLIF(cr.rate, 0), 1.0) * l.product_qty)::decimal(16,2) as price_total, - avg(100.0 * (l.price_unit / COALESCE(NULLIF(cr.rate, 0),1.0) * l.product_qty) / NULLIF(ip.value_float*l.product_qty/u.factor*u2.factor, 0.0))::decimal(16,2) as negociation, - sum(ip.value_float*l.product_qty/u.factor*u2.factor)::decimal(16,2) as price_standard, - (sum(l.product_qty * l.price_unit / COALESCE(NULLIF(cr.rate, 0), 1.0))/NULLIF(sum(l.product_qty/u.factor*u2.factor),0.0))::decimal(16,2) as price_average, + sum(l.price_total / COALESCE(po.currency_rate, 1.0))::decimal(16,2) 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) as price_average, partner.country_id as country_id, partner.commercial_partner_id as commercial_partner_id, analytic_account.id as account_analytic_id, - sum(p.weight * l.product_qty/u.factor*u2.factor) as weight, - sum(p.volume * l.product_qty/u.factor*u2.factor) as volume + 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) 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, + sum(l.product_qty / line_uom.factor * product_uom.factor) - sum(l.qty_received / line_uom.factor * product_uom.factor) as qty_to_be_billed """ % self.env['res.currency']._select_companies_rates() return select_str def _from(self): from_str = """ purchase_order_line l - join purchase_order s on (l.order_id=s.id) - join res_partner partner on s.partner_id = partner.id + join purchase_order po on (l.order_id=po.id) + join res_partner partner on po.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 ir_property ip ON (ip.name='standard_price' AND ip.res_id=CONCAT('product.product,',p.id) AND ip.company_id=s.company_id) - left join uom_uom u on (u.id=l.product_uom) - left join uom_uom u2 on (u2.id=t.uom_id) + left join uom_uom line_uom on (line_uom.id=l.product_uom) + left join uom_uom product_uom on (product_uom.id=t.uom_id) left join account_analytic_account analytic_account on (l.account_analytic_id = analytic_account.id) - left join currency_rate cr on (cr.currency_id = s.currency_id and - cr.company_id = s.company_id and - cr.date_start <= coalesce(s.date_order, now()) and - (cr.date_end is null or cr.date_end > coalesce(s.date_order, now()))) + left join currency_rate cr on (cr.currency_id = po.currency_id and + cr.company_id = po.company_id and + cr.date_start <= coalesce(po.date_order, now()) and + (cr.date_end is null or cr.date_end > coalesce(po.date_order, now()))) """ return from_str def _group_by(self): group_by_str = """ GROUP BY - s.company_id, - s.user_id, - s.partner_id, - u.factor, - s.currency_id, + po.company_id, + po.user_id, + po.partner_id, + line_uom.factor, + po.currency_id, l.price_unit, - s.date_approve, + po.date_approve, l.date_planned, l.product_uom, - s.dest_address_id, - s.fiscal_position_id, + po.dest_address_id, + po.fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id, - s.date_order, - s.state, - u.uom_type, - u.category_id, + po.date_order, + po.state, + line_uom.uom_type, + line_uom.category_id, t.uom_id, - u.id, - u2.factor, + line_uom.id, + product_uom.factor, partner.country_id, partner.commercial_partner_id, - analytic_account.id + analytic_account.id, + po.id """ return group_by_str diff --git a/addons/purchase/report/purchase_report_views.xml b/addons/purchase/report/purchase_report_views.xml index f4cc05af27f..bc15d2c589c 100644 --- a/addons/purchase/report/purchase_report_views.xml +++ b/addons/purchase/report/purchase_report_views.xml @@ -4,12 +4,11 @@ product.month.pivot purchase.report - - - + + + + - - @@ -17,10 +16,9 @@ product.month.graph purchase.report - - - - + + + @@ -31,7 +29,6 @@ purchase.report [('state','!=','cancel')] - {'group_by': ['category_id'], 'col_group_by': ['date_order:month'], 'measures': ['unit_quantity']} Price Per Vendor @@ -54,8 +51,8 @@ purchase.report - - + + @@ -69,12 +66,13 @@ + - + @@ -90,7 +88,8 @@ current - + + + diff --git a/addons/purchase/tests/__init__.py b/addons/purchase/tests/__init__.py new file mode 100644 index 00000000000..d8e61f341b9 --- /dev/null +++ b/addons/purchase/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_purchase_order_report diff --git a/addons/purchase/tests/test_purchase_order_report.py b/addons/purchase/tests/test_purchase_order_report.py new file mode 100644 index 00000000000..24750e71d60 --- /dev/null +++ b/addons/purchase/tests/test_purchase_order_report.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from odoo.tests import common, Form, tagged + + +@tagged('post_install', '-at_install') +class TestPurchaseOrderReport(common.TransactionCase): + def setUp(self): + super(TestPurchaseOrderReport, self).setUp() + + self.partner_id = self.env.ref('base.res_partner_1') + self.product1 = self.env.ref('product.product_product_8') + self.product2 = self.env.ref('product.product_product_11') + self.PurchaseReport = self.env['purchase.report'] + + # Create a new company and set CoA + self.company_id = self.env['res.company'].create({'name': 'new_company'}) + self.env.user.company_id = self.company_id + self.env.ref('l10n_generic_coa.configurable_chart_template').load_for_current_company(False, False) + + def test_00_purchase_order_report(self): + uom_dozen = self.env.ref('uom.product_uom_dozen') + + eur_currency = self.env.ref('base.EUR') + self.company_id.currency_id = self.env.ref('base.USD').id + + self.env['res.currency.rate'].search([]).unlink() + self.env['res.currency.rate'].create({ + 'name': datetime.today(), + 'rate': 2.0, + 'currency_id': eur_currency.id, + }) + po = self.env['purchase.order'].create({ + 'partner_id': self.partner_id.id, + 'currency_id': eur_currency.id, + 'order_line': [ + (0, 0, { + 'name': self.product1.name, + 'product_id': self.product1.id, + 'product_qty': 1.0, + 'product_uom': uom_dozen.id, + 'price_unit': 100.0, + 'date_planned': datetime.today(), + }), + (0, 0, { + 'name': self.product2.name, + 'product_id': self.product2.id, + 'product_qty': 1.0, + 'product_uom': uom_dozen.id, + 'price_unit': 200.0, + 'date_planned': datetime.today(), + }), + ], + }) + po.button_confirm() + + f = Form(self.env['account.invoice']) + f.partner_id = po.partner_id + f.purchase_id = po + invoice = f.save() + invoice.action_invoice_open() + + res_product1 = self.PurchaseReport.search([ + ('order_id', '=', po.id), ('product_id', '=', self.product1.id)]) + + # check that report will convert dozen to unit or not + self.assertEquals(res_product1.qty_ordered, 12.0, 'UoM conversion is not working') + # report should show in company currency (amount/rate) = (100/2) + self.assertEquals(res_product1.price_total, 50.0, 'Currency conversion is not working') + + res_product2 = self.PurchaseReport.search([ + ('order_id', '=', po.id), ('product_id', '=', self.product2.id)]) + + # Check that repost should show 6 unit of product + self.assertEquals(res_product2.qty_ordered, 12.0, 'UoM conversion is not working') + # report should show in company currency (amount/rate) = (200/2) + self.assertEquals(res_product2.price_total, 100.0, 'Currency conversion is not working') diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index ea23fb1ea0a..edaa5693f3f 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -175,7 +175,8 @@ - + + @@ -374,7 +375,8 @@ - + + @@ -420,6 +422,7 @@ purchase.order tree,kanban,form,pivot,graph,calendar,activity + {'quotation_only': True}

Create a request for quotation diff --git a/addons/purchase_stock/report/purchase_report.py b/addons/purchase_stock/report/purchase_report.py index defb1a6c167..46254cadca9 100644 --- a/addons/purchase_stock/report/purchase_report.py +++ b/addons/purchase_stock/report/purchase_report.py @@ -13,7 +13,7 @@ class PurchaseReport(models.Model): return super(PurchaseReport, self)._select() + ", spt.warehouse_id as picking_type_id" def _from(self): - return super(PurchaseReport, self)._from() + " left join stock_picking_type spt on (spt.id=s.picking_type_id)" + 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"