diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 231d37b9b3d..0d8d9ec9bf1 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -36,11 +36,9 @@ class AccountMove(models.Model):
if not self.purchase_id:
return
- # Copy partner.
- self.partner_id = self.purchase_id.partner_id
- self.fiscal_position_id = self.purchase_id.fiscal_position_id
- self.invoice_payment_term_id = self.purchase_id.payment_term_id
- self.currency_id = self.purchase_id.currency_id
+ # Copy data from PO
+ invoice_vals = self.purchase_id._prepare_invoice()
+ self.update(invoice_vals)
# Copy purchase lines.
po_lines = self.purchase_id.order_line - self.line_ids.mapped('purchase_line_id')
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 545f928b327..868dfe37b26 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -3,11 +3,12 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
+from itertools import groupby
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.osv import expression
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
-from odoo.tools.float_utils import float_compare
+from odoo.tools.float_utils import float_compare, float_is_zero
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.misc import formatLang, get_lang
@@ -31,7 +32,7 @@ class PurchaseOrder(models.Model):
'amount_total': amount_untaxed + amount_tax,
})
- @api.depends('state', 'order_line.qty_invoiced', 'order_line.qty_received', 'order_line.product_qty')
+ @api.depends('state', 'order_line.qty_to_invoice')
def _get_invoiced(self):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for order in self:
@@ -39,13 +40,9 @@ class PurchaseOrder(models.Model):
order.invoice_status = 'no'
continue
- if any(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) == -1 for line in order.order_line):
+ if any(not float_is_zero(line.qty_to_invoice, precision_digits=precision) for line in order.order_line):
order.invoice_status = 'to invoice'
- elif all(
- (line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received)
- and float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) >= 0
- for line in order.order_line
- ):
+ elif all(float_is_zero(line.qty_to_invoice, precision_digits=precision) for line in order.order_line) and order.invoice_ids:
order.invoice_status = 'invoiced'
else:
order.invoice_status = 'no'
@@ -402,39 +399,127 @@ class PurchaseOrder(models.Model):
except AccessError: # no write access rights -> just ignore
break
- def action_view_invoice(self):
- '''
- This function returns an action that display existing vendor bills of given purchase order ids.
- When only one found, show the vendor bill immediately.
- '''
+ def action_create_invoice(self):
+ """Create the invoice associated to the PO.
+ """
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+
+ # 1) Prepare invoice vals and clean-up the section lines
+ invoice_vals_list = []
+ for order in self:
+ if order.invoice_status != 'to invoice':
+ continue
+
+ pending_section = None
+ # Invoice values.
+ invoice_vals = order._prepare_invoice()
+ # Invoice line values (keep only necessary sections).
+ for line in order.order_line:
+ if line.display_type == 'line_section':
+ pending_section = line
+ continue
+ if not float_is_zero(line.qty_to_invoice, precision_digits=precision):
+ if pending_section:
+ invoice_vals['invoice_line_ids'].append((0, 0, pending_section._prepare_account_move_line()))
+ pending_section = None
+ invoice_vals['invoice_line_ids'].append((0, 0, line._prepare_account_move_line()))
+ invoice_vals_list.append(invoice_vals)
+
+ if not invoice_vals_list:
+ raise UserError(_('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.'))
+
+ # 2) group by (company_id, partner_id, currency_id) for batch creation
+ new_invoice_vals_list = []
+ for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: (x.get('company_id'), x.get('partner_id'), x.get('currency_id'))):
+ origins = set()
+ payment_refs = set()
+ refs = set()
+ ref_invoice_vals = None
+ for invoice_vals in invoices:
+ if not ref_invoice_vals:
+ ref_invoice_vals = invoice_vals
+ else:
+ ref_invoice_vals['invoice_line_ids'] += invoice_vals['invoice_line_ids']
+ origins.add(invoice_vals['invoice_origin'])
+ payment_refs.add(invoice_vals['invoice_payment_ref'])
+ refs.add(invoice_vals['ref'])
+ ref_invoice_vals.update({
+ 'ref': ', '.join(refs)[:2000],
+ 'invoice_origin': ', '.join(origins),
+ 'invoice_payment_ref': len(payment_refs) == 1 and payment_refs.pop() or False,
+ })
+ new_invoice_vals_list.append(ref_invoice_vals)
+ invoice_vals_list = new_invoice_vals_list
+
+ # 3) Create invoices.
+ moves = self.env['account.move']
+ AccountMove = self.env['account.move'].with_context(default_move_type='in_invoice')
+ for vals in invoice_vals_list:
+ moves |= AccountMove.with_company(vals['company_id']).create(vals)
+
+ # 4) Some moves might actually be refunds: convert them if the total amount is negative
+ # We do this after the moves have been created since we need taxes, etc. to know if the total
+ # is actually negative or not
+ moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note()
+
+ return self.action_view_invoice(moves)
+
+ def _prepare_invoice(self):
+ """Prepare the dict of values to create the new invoice for a purchase order.
+ """
+ self.ensure_one()
+ self = self.with_company(self.company_id)
+ move_type = self._context.get('default_move_type', 'in_invoice')
+ journal = self.env['account.move'].with_context(default_move_type=move_type)._get_default_journal()
+ if not journal:
+ raise UserError(_('Please define an accounting purchase journal for the company %s (%s).') % (self.company_id.name, self.company_id.id))
+
+ partner_invoice_id = self.partner_id.address_get(['invoice'])['invoice']
+ invoice_vals = {
+ 'ref': self.partner_ref or '',
+ 'move_type': move_type,
+ 'narration': self.notes,
+ 'currency_id': self.currency_id.id,
+ 'invoice_user_id': self.user_id and self.user_id.id,
+ 'partner_id': partner_invoice_id,
+ 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(partner_invoice_id)).id,
+ 'invoice_payment_ref': self.partner_ref or '',
+ 'invoice_partner_bank_id': self.partner_id.bank_ids[:1].id,
+ 'invoice_origin': self.name,
+ 'invoice_payment_term_id': self.payment_term_id.id,
+ 'invoice_line_ids': [],
+ 'company_id': self.company_id
+ }
+ return invoice_vals
+
+ def action_view_invoice(self, invoices=False):
+ """This function returns an action that display existing vendor bills of
+ given purchase order ids. When only one found, show the vendor bill
+ immediately.
+ """
+ if not invoices:
+ # Invoice_ids may be filtered depending on the user. To ensure we get all
+ # invoices related to the purchase order, we read them in sudo to fill the
+ # cache.
+ self.sudo()._read(['invoice_ids'])
+ invoices = self.invoice_ids
+
action = self.env.ref('account.action_move_in_invoice_type')
result = action.read()[0]
- create_bill = self.env.context.get('create_bill', False)
- # override the context to get rid of the default filtering
- result['context'] = {
- 'default_move_type': 'in_invoice',
- 'default_company_id': self.company_id.id,
- 'default_purchase_id': self.id,
- }
- # Invoice_ids may be filtered depending on the user. To ensure we get all
- # invoices related to the purchase order, we read them in sudo to fill the
- # cache.
- self.sudo()._read(['invoice_ids'])
# choose the view_mode accordingly
- if len(self.invoice_ids) > 1 and not create_bill:
- result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
- else:
+ if len(invoices) > 1:
+ result['domain'] = [('id', 'in', invoices.ids)]
+ elif len(invoices) == 1:
res = self.env.ref('account.view_move_form', False)
form_view = [(res and res.id or False, 'form')]
if 'views' in result:
- result['views'] = form_view + [(state,view) for state,view in action['views'] if view != 'form']
+ result['views'] = form_view + [(state, view) for state, view in action['views'] if view != 'form']
else:
result['views'] = form_view
- # Do not set an invoice_id if we want to create a new bill.
- if not create_bill:
- result['res_id'] = self.invoice_ids.id or False
- result['context']['default_invoice_origin'] = self.name
- result['context']['default_ref'] = self.partner_ref
+ result['res_id'] = invoices.id
+ else:
+ result = {'type': 'ir.actions.act_window_close'}
+
return result
@@ -476,6 +561,8 @@ class PurchaseOrderLine(models.Model):
" - Stock Moves: the quantity comes from confirmed pickings\n")
qty_received = fields.Float("Received Qty", compute='_compute_qty_received', inverse='_inverse_qty_received', compute_sudo=True, store=True, digits='Product Unit of Measure')
qty_received_manual = fields.Float("Manual Received Qty", digits='Product Unit of Measure', copy=False)
+ qty_to_invoice = fields.Float(compute='_compute_qty_invoiced', string='To Invoice Quantity', store=True, readonly=True,
+ digits='Product Unit of Measure')
partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True)
currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True)
@@ -533,9 +620,10 @@ class PurchaseOrderLine(models.Model):
taxes = line.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == line.env.company)
line.taxes_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_id)
- @api.depends('invoice_lines.move_id.state', 'invoice_lines.quantity')
+ @api.depends('invoice_lines.move_id.state', 'invoice_lines.quantity', 'qty_received', 'product_uom_qty', 'order_id.state')
def _compute_qty_invoiced(self):
for line in self:
+ # compute qty_invoiced
qty = 0.0
for inv_line in line.invoice_lines:
if inv_line.move_id.state not in ['cancel']:
@@ -545,6 +633,15 @@ class PurchaseOrderLine(models.Model):
qty -= inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom)
line.qty_invoiced = qty
+ # compute qty_to_invoice
+ if line.order_id.state in ['purchase', 'done']:
+ if line.product_id.purchase_method == 'purchase':
+ line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
+ else:
+ line.qty_to_invoice = line.qty_received - line.qty_invoiced
+ else:
+ line.qty_to_invoice = 0
+
@api.depends('product_id')
def _compute_qty_received_method(self):
for line in self:
@@ -734,36 +831,36 @@ class PurchaseOrderLine(models.Model):
return name
- def _prepare_account_move_line(self, move):
+ def _prepare_account_move_line(self, move=False):
self.ensure_one()
- if self.product_id.purchase_method == 'purchase':
- qty = self.product_qty - self.qty_invoiced
- else:
- qty = self.qty_received - self.qty_invoiced
- if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0:
- qty = 0.0
+ res = {
+ 'display_type': self.display_type,
+ 'sequence': self.sequence,
+ 'name': '%s: %s' % (self.order_id.name, self.name),
+ 'product_id': self.product_id.id,
+ 'product_uom_id': self.product_uom.id,
+ 'quantity': self.qty_to_invoice,
+ 'price_unit': self.price_unit,
+ 'tax_ids': [(6, 0, self.taxes_id.ids)],
+ 'analytic_account_id': self.account_analytic_id.id,
+ 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
+ 'purchase_line_id': self.id,
+ }
+ if not move:
+ return res
if self.currency_id == move.company_id.currency_id:
currency = False
else:
currency = move.currency_id
- return {
- 'name': '%s: %s' % (self.order_id.name, self.name),
+ res.update({
'move_id': move.id,
'currency_id': currency and currency.id or False,
- 'purchase_line_id': self.id,
'date_maturity': move.invoice_date_due,
- 'product_uom_id': self.product_uom.id,
- 'product_id': self.product_id.id,
- 'price_unit': self.price_unit,
- 'quantity': qty,
'partner_id': move.partner_id.id,
- 'analytic_account_id': self.account_analytic_id.id,
- 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
- 'tax_ids': [(6, 0, self.taxes_id.ids)],
- 'display_type': self.display_type,
- }
+ })
+ return res
@api.model
def _prepare_add_missing_fields(self, values):
diff --git a/addons/purchase/tests/__init__.py b/addons/purchase/tests/__init__.py
index f1705c0370f..b97052ae2fd 100644
--- a/addons/purchase/tests/__init__.py
+++ b/addons/purchase/tests/__init__.py
@@ -3,4 +3,5 @@
from . import test_purchase
from . import test_purchase_order_report
+from . import test_purchase_invoice
from . import test_access_rights
diff --git a/addons/purchase/tests/test_access_rights.py b/addons/purchase/tests/test_access_rights.py
index af431a9c71c..d7251f4d007 100644
--- a/addons/purchase/tests/test_access_rights.py
+++ b/addons/purchase/tests/test_access_rights.py
@@ -61,11 +61,9 @@ class TestPurchaseInvoice(SavepointCase):
purchase_order = purchase_order_form.save()
purchase_order.button_confirm()
- action = purchase_order.with_user(self.purchase_user).action_view_invoice()
- invoice_form = Form(self.env['account.move'].with_user(self.purchase_user).with_context(
- action['context']
- ))
- invoice = invoice_form.save()
+ purchase_order.order_line.qty_received = 4
+ purchase_order.action_create_invoice()
+ invoice = purchase_order.invoice_ids
with self.assertRaises(AccessError):
invoice.post()
@@ -86,9 +84,11 @@ class TestPurchaseInvoice(SavepointCase):
line.price_unit = 5
purchase_order_user2 = purchase_order_form.save()
- action = purchase_order_user2.with_user(purchase_user_2).action_view_invoice()
- invoice_form = Form(self.env['account.move'].with_user(purchase_user_2).with_context(action['context']))
- vendor_bill_user2 = invoice_form.save()
+ purchase_order_user2.button_confirm()
+
+ purchase_order_user2.order_line.qty_received = 4
+ purchase_order_user2.action_create_invoice()
+ vendor_bill_user2 = purchase_order_user2.invoice_ids
# open purchase_order_user2 and vendor_bill_user2 with `self.purchase_user`
purchase_order_user1 = Form(purchase_order_user2.with_user(self.purchase_user))
@@ -121,9 +121,11 @@ class TestPurchaseInvoice(SavepointCase):
line.price_unit = 5
purchase_order_user2 = purchase_order_form.save()
- action = purchase_order_user2.with_user(purchase_user_2).action_view_invoice()
- invoice_form = Form(self.env['account.move'].with_user(purchase_user_2).with_context(action['context']))
- vendor_bill_user2 = invoice_form.save()
+ purchase_order_user2.button_confirm()
+
+ purchase_order_user2.order_line.qty_received = 4
+ purchase_order_user2.action_create_invoice()
+ vendor_bill_user2 = purchase_order_user2.invoice_ids
# check user 1 cannot read the invoice
with self.assertRaises(AccessError):
diff --git a/addons/purchase/tests/test_purchase_invoice.py b/addons/purchase/tests/test_purchase_invoice.py
new file mode 100644
index 00000000000..f210444e042
--- /dev/null
+++ b/addons/purchase/tests/test_purchase_invoice.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.account.tests.common import AccountTestNoChartCommon
+
+
+class TestCommonPurchaseNoChart(AccountTestNoChartCommon):
+ """ This class should be extended for test suite of purchase flows with a minimal chart of accounting
+ installed. This test suite should be executed at module installation.
+ This class provides some method to generate testing data well configured, according to the minimal
+ chart of account, defined in `AccountTestNoChartCommon` class.
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestCommonPurchaseNoChart, cls).setUpClass()
+
+ # create a vendor
+ context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True}
+ Partner = cls.env['res.partner'].with_context(context_no_mail)
+ cls.vendor = Partner.create({
+ 'name': 'vendor',
+ 'email': 'vendor@vendor.com',
+ 'property_account_payable_id': cls.account_payable.id,
+ 'property_account_receivable_id': cls.account_receivable.id,
+ 'company_id': cls.env.ref('base.main_company').id
+ })
+
+ @classmethod
+ def setUpClassicProducts(cls):
+ uom_unit = cls.env.ref('uom.product_uom_unit')
+ uom_hour = cls.env.ref('uom.product_uom_hour')
+ cls.product_order = cls.env['product.product'].create({
+ 'name': "Zed+ Antivirus",
+ 'standard_price': 235.0,
+ 'list_price': 280.0,
+ 'type': 'consu',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ 'purchase_method': 'purchase',
+ 'default_code': 'PROD_ORDER',
+ 'taxes_id': False,
+ # 'categ_id': cls.product_category.id,
+ })
+ cls.service_deliver = cls.env['product.product'].create({
+ 'name': "Cost-plus Contract",
+ 'standard_price': 200.0,
+ 'list_price': 180.0,
+ 'type': 'service',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ 'purchase_method': 'receive',
+ 'default_code': 'SERV_DEL',
+ 'taxes_id': False,
+ # 'categ_id': cls.product_category.id,
+ })
+ cls.service_order = cls.env['product.product'].create({
+ 'name': "Prepaid Consulting",
+ 'standard_price': 40.0,
+ 'list_price': 90.0,
+ 'type': 'service',
+ 'uom_id': uom_hour.id,
+ 'uom_po_id': uom_hour.id,
+ 'purchase_method': 'purchase',
+ 'default_code': 'PRE-PAID',
+ 'taxes_id': False,
+ # 'categ_id': cls.product_category.id,
+ })
+ cls.product_deliver = cls.env['product.product'].create({
+ 'name': "Switch, 24 ports",
+ 'standard_price': 55.0,
+ 'list_price': 70.0,
+ 'type': 'consu',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ 'purchase_method': 'receive',
+ 'default_code': 'PROD_DEL',
+ 'taxes_id': False,
+ # 'categ_id': cls.product_category.id,
+ })
+
+
+class TestPurchaseToInvoice(TestCommonPurchaseNoChart):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestPurchaseToInvoice, cls).setUpClass()
+ cls.setUpClassicProducts()
+ cls.setUpAdditionalAccounts()
+ cls.setUpAccountJournal()
+
+ def test_vendor_bill_delivered(self):
+ """Test if a order of product invoiced by delivered quantity can be
+ correctly invoiced."""
+ purchase_order = self.env['purchase.order'].with_context(tracking_disable=True).create({
+ 'partner_id': self.vendor.id,
+ })
+ PurchaseOrderLine = self.env['purchase.order.line'].with_context(tracking_disable=True)
+ pol_prod_deliver = PurchaseOrderLine.create({
+ 'name': self.product_deliver.name,
+ 'product_id': self.product_deliver.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product_deliver.uom_id.id,
+ 'price_unit': self.product_deliver.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ pol_serv_deliver = PurchaseOrderLine.create({
+ 'name': self.service_deliver.name,
+ 'product_id': self.service_deliver.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.service_deliver.uom_id.id,
+ 'price_unit': self.service_deliver.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ purchase_order.button_confirm()
+
+ self.assertEqual(purchase_order.invoice_status, "no")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 0.0)
+
+ purchase_order.order_line.qty_received = 5
+ self.assertEqual(purchase_order.invoice_status, "to invoice")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 5)
+ self.assertEqual(line.qty_invoiced, 0.0)
+
+ purchase_order.action_create_invoice()
+ self.assertEqual(purchase_order.invoice_status, "invoiced")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 5)
+
+ def test_vendor_bill_ordered(self):
+ """Test if a order of product invoiced by ordered quantity can be
+ correctly invoiced."""
+ purchase_order = self.env['purchase.order'].with_context(tracking_disable=True).create({
+ 'partner_id': self.vendor.id,
+ })
+ PurchaseOrderLine = self.env['purchase.order.line'].with_context(tracking_disable=True)
+ pol_prod_order = PurchaseOrderLine.create({
+ 'name': self.product_order.name,
+ 'product_id': self.product_order.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product_order.uom_id.id,
+ 'price_unit': self.product_order.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ pol_serv_order = PurchaseOrderLine.create({
+ 'name': self.service_order.name,
+ 'product_id': self.service_order.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.service_order.uom_id.id,
+ 'price_unit': self.service_order.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ purchase_order.button_confirm()
+
+ self.assertEqual(purchase_order.invoice_status, "to invoice")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 10)
+ self.assertEqual(line.qty_invoiced, 0.0)
+
+ purchase_order.order_line.qty_received = 5
+ self.assertEqual(purchase_order.invoice_status, "to invoice")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 10)
+ self.assertEqual(line.qty_invoiced, 0.0)
+
+ purchase_order.action_create_invoice()
+ self.assertEqual(purchase_order.invoice_status, "invoiced")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 10)
+
+ def test_vendor_bill_delivered_return(self):
+ """Test when return product, a order of product invoiced by delivered
+ quantity can be correctly invoiced."""
+ purchase_order = self.env['purchase.order'].with_context(tracking_disable=True).create({
+ 'partner_id': self.vendor.id,
+ })
+ PurchaseOrderLine = self.env['purchase.order.line'].with_context(tracking_disable=True)
+ pol_prod_deliver = PurchaseOrderLine.create({
+ 'name': self.product_deliver.name,
+ 'product_id': self.product_deliver.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product_deliver.uom_id.id,
+ 'price_unit': self.product_deliver.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ pol_serv_deliver = PurchaseOrderLine.create({
+ 'name': self.service_deliver.name,
+ 'product_id': self.service_deliver.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.service_deliver.uom_id.id,
+ 'price_unit': self.service_deliver.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ purchase_order.button_confirm()
+
+ purchase_order.order_line.qty_received = 10
+ purchase_order.action_create_invoice()
+ self.assertEqual(purchase_order.invoice_status, "invoiced")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 10)
+
+ purchase_order.order_line.qty_received = 5
+ self.assertEqual(purchase_order.invoice_status, "to invoice")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, -5)
+ self.assertEqual(line.qty_invoiced, 10)
+ purchase_order.action_create_invoice()
+ self.assertEqual(purchase_order.invoice_status, "invoiced")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 5)
+
+ def test_vendor_bill_ordered_return(self):
+ """Test when return product, a order of product invoiced by ordered
+ quantity can be correctly invoiced."""
+ purchase_order = self.env['purchase.order'].with_context(tracking_disable=True).create({
+ 'partner_id': self.vendor.id,
+ })
+ PurchaseOrderLine = self.env['purchase.order.line'].with_context(tracking_disable=True)
+ pol_prod_order = PurchaseOrderLine.create({
+ 'name': self.product_order.name,
+ 'product_id': self.product_order.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product_order.uom_id.id,
+ 'price_unit': self.product_order.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ pol_serv_order = PurchaseOrderLine.create({
+ 'name': self.service_order.name,
+ 'product_id': self.service_order.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.service_order.uom_id.id,
+ 'price_unit': self.service_order.list_price,
+ 'order_id': purchase_order.id,
+ 'taxes_id': False,
+ })
+ purchase_order.button_confirm()
+
+ purchase_order.order_line.qty_received = 10
+ purchase_order.action_create_invoice()
+ self.assertEqual(purchase_order.invoice_status, "invoiced")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 10)
+
+ purchase_order.order_line.qty_received = 5
+ self.assertEqual(purchase_order.invoice_status, "invoiced")
+ for line in purchase_order.order_line:
+ self.assertEqual(line.qty_to_invoice, 0.0)
+ self.assertEqual(line.qty_invoiced, 10)
diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml
index 273d3fe4360..de63c1550c4 100644
--- a/addons/purchase/views/purchase_views.xml
+++ b/addons/purchase/views/purchase_views.xml
@@ -127,12 +127,12 @@
-
+
-
+
@@ -652,4 +652,16 @@
+
+
+ Create Vendor Bills
+
+
+
+ code
+
+ if records:
+ action = records.action_create_invoice()
+
+
diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index f5b215281bb..7bca1d03090 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -135,10 +135,10 @@ class PurchaseOrder(models.Model):
result['res_id'] = pick_ids.id
return result
- def action_view_invoice(self):
- result = super().action_view_invoice()
- result['context']['default_invoice_incoterm_id'] = self.incoterm_id.id
- return result
+ def _prepare_invoice(self):
+ invoice_vals = super()._prepare_invoice()
+ invoice_vals['invoice_incoterm_id'] = self.incoterm_id.id
+ return invoice_vals
# --------------------------------------------------
# Business methods
diff --git a/addons/stock_landed_costs/tests/test_stockvaluationlayer.py b/addons/stock_landed_costs/tests/test_stockvaluationlayer.py
index fa459a0f932..d0401bab47a 100644
--- a/addons/stock_landed_costs/tests/test_stockvaluationlayer.py
+++ b/addons/stock_landed_costs/tests/test_stockvaluationlayer.py
@@ -278,9 +278,8 @@ class TestStockValuationLCFIFOVB(TestStockValuationLCCommon):
self.assertEqual(valuation_aml.credit, 0)
# Create a vebdor bill for the RFQ
- action = rfq.action_view_invoice()
- vb = Form(self.env['account.move'].with_context(action['context']))
- vb = vb.save()
+ action = rfq.action_create_invoice()
+ vb = self.env['account.move'].search([('id', '=', action['res_id'])])
vb.post()
input_aml = self._get_stock_input_move_lines()[-1]
@@ -366,9 +365,9 @@ class TestStockValuationLCFIFOVB(TestStockValuationLCCommon):
self.assertEqual(valuation_aml.debit, 100)
self.assertEqual(valuation_aml.credit, 0)
- # Create a vebdor bill for the RFQ and add to it the landed cost
- action = rfq.action_view_invoice()
- vb = Form(self.env['account.move'].with_context(action['context']))
+ # Create a vendor bill for the RFQ and add to it the landed cost
+ vb = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
+ vb.partner_id = self.vendor1
with vb.invoice_line_ids.new() as inv_line:
inv_line.product_id = self.productlc1
inv_line.price_unit = 50
@@ -419,9 +418,8 @@ class TestStockValuationLCFIFOVB(TestStockValuationLCCommon):
self.assertEqual(valuation_aml.credit, 0)
# Create a vebdor bill for the RFQ
- action = rfq.action_view_invoice()
- vb = Form(self.env['account.move'].with_context(action['context']))
- vb = vb.save()
+ action = rfq.action_create_invoice()
+ vb = self.env['account.move'].search([('id', '=', action['res_id'])])
vb.post()
expense_aml = self._get_expense_move_lines()[-1]