[IMP] purchase: make billing consitent with sale invoicing
======== Purpose ======== Currently, purchase missing some billing functions that we have in sale invoicing: - Create credit note when necessary - Create vendor bill in batch in list view ======== Spec ======== - In case of retures, when create a bill for a purchase order, we check the total amount we want to bill to decide whether it's a vendor bill or a credit note. - In the list view, add a new action to create vendor bill/credit note for all selected order. ======== Links ======== Task 2170715 PR #44210 Related: odoo/upgrade#919 Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
|
||||
from . import test_purchase
|
||||
from . import test_purchase_order_report
|
||||
from . import test_purchase_invoice
|
||||
from . import test_access_rights
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -127,12 +127,12 @@
|
||||
<button name="print_quotation" string="Print RFQ" type="object" states="draft" class="oe_highlight" groups="base.group_user"/>
|
||||
<button name="button_confirm" type="object" states="sent" string="Confirm Order" class="oe_highlight" id="bid_confirm"/>
|
||||
<button name="button_approve" type="object" states='to approve' string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/>
|
||||
<button name="action_view_invoice" string="Create Bill" type="object" class="oe_highlight" context="{'create_bill':True}" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'in', ('no', 'invoiced'))]}"/>
|
||||
<button name="action_create_invoice" string="Create Bill" type="object" class="oe_highlight" context="{'create_bill':True}" attrs="{'invisible': ['|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'in', ('no', 'invoiced'))]}"/>
|
||||
<button name="action_rfq_send" states="sent" string="Re-Send by Email" type="object" context="{'send_rfq':True}"/>
|
||||
<button name="print_quotation" string="Print RFQ" type="object" states="sent" groups="base.group_user"/>
|
||||
<button name="button_confirm" type="object" states="draft" string="Confirm Order" id="draft_confirm"/>
|
||||
<button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}"/>
|
||||
<button name="action_view_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}"/>
|
||||
<button name="action_create_invoice" string="Create Bill" type="object" context="{'create_bill':True}" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('invoice_status', 'not in', ('no', 'invoiced')), ('order_line', '=', [])]}"/>
|
||||
<button name="button_draft" states="cancel" string="Set to Draft" type="object" />
|
||||
<button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
|
||||
<button name="button_done" type="object" string="Lock" states="purchase"/>
|
||||
@@ -652,4 +652,16 @@
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_purchase_batch_bills" model="ir.actions.server">
|
||||
<field name="name">Create Vendor Bills</field>
|
||||
<field name="model_id" ref="purchase.model_purchase_order"/>
|
||||
<field name="binding_model_id" ref="purchase.model_purchase_order"/>
|
||||
<field name='groups_id' eval="[(4, ref('account.group_account_invoice'))]"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records.action_create_invoice()
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user