openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #02270
lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe into lp:account-invoicing
Alexandre Fayolle - camptocamp has proposed merging lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe into lp:account-invoicing.
Requested reviews:
Leonardo Pistone @ camptocamp (lpistone): code review
Nicolas Bessi - Camptocamp (nbessi-c2c)
For more details, see:
https://code.launchpad.net/~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe/+merge/199282
[ADD] sale_order_partial_invoice
This module allows invoicing a partial quantity of Sale Order lines
>From the module description:
With a sale order in 'manual' invoicing policy, when the user selects to
invoice some SO lines, a new wizard is display in which it is possible to
select how much of the different lines is to be invoiced. The amounts
invoiced and the amounts delivered are also displayed. When generating an
invoice for the whole sale order, the partial invoices are taken into
account and only the amounts not already invoiced are part of the new
invoice
--
https://code.launchpad.net/~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe/+merge/199282
Your team Account Core Editors is subscribed to branch lp:account-invoicing.
=== added directory 'sale_order_partial_invoice'
=== added file 'sale_order_partial_invoice/__init__.py'
--- sale_order_partial_invoice/__init__.py 1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/__init__.py 2013-12-17 13:52:28 +0000
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Alexandre Fayolle
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import sale
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
=== added file 'sale_order_partial_invoice/__openerp__.py'
--- sale_order_partial_invoice/__openerp__.py 1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/__openerp__.py 2013-12-17 13:52:28 +0000
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Alexandre Fayolle
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ 'name': 'Sale Partial Invoice',
+ 'version': '1.1',
+ 'category': 'Accounting & Finance',
+ 'description': """Allow to partialy invoice Sale Order lines
+
+ With a sale order in 'manual' invoicing policy, when the user selects to
+ invoice some SO lines, a new wizard is display in which it is possible to
+ select how much of the different lines is to be invoiced. The amounts
+ invoiced and the amounts delivered are also displayed. When generating an
+ invoice for the whole sale order, the partial invoices are taken into
+ account and only the amounts not already invoiced are part of the new
+ invoice
+ """,
+ 'author': 'Camptocamp',
+ 'website': 'http://www.camptocamp.com',
+ 'license': 'AGPL-3',
+ 'depends': ['sale', 'account', 'sale_stock'],
+ 'data': [
+ 'sale_view.xml',
+ ],
+ 'test': [
+ 'test/partial_so_invoice.yml',
+ 'test/make_remaining_invoice.yml',
+ ],
+ 'demo': [],
+ 'installable': True,
+ 'active': False,
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
=== added directory 'sale_order_partial_invoice/i18n'
=== added file 'sale_order_partial_invoice/sale.py'
--- sale_order_partial_invoice/sale.py 1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/sale.py 2013-12-17 13:52:28 +0000
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Alexandre Fayolle
+# Copyright 2013 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+"""
+ * Adding qty_invoiced field on SO lines, computed based on invoice lines
+ linked to it that has the same product. So this way, advance invoice will
+ still work !
+
+ * Adding qty_delivered field in SO Lines, computed from move lines linked to
+ it. For services, the quantity delivered is a problem, the MRP will
+ automatically run the procurement linked to this line and pass it to done. I
+ suggest that in that case, delivered qty = invoiced_qty as the procurement
+ is for the whole qty, it'll be a good alternative to follow what has been
+ done and not.
+
+ * Add in the "Order Line to invoice" view those fields
+
+ * Change the behavior of the "invoiced" field of the SO line to be true when
+ all is invoiced
+
+ * Adapt the "_make_invoice" method in SO to deal with qty_invoiced
+
+ * Adapt the sale_line_invoice.py wizard to deal with qty_invoiced, asking the
+ user how much he want to invoice.
+
+By having the delivered quantity, we can imagine in the future to provide an
+invoicing "based on delivery" that will look at those values instead of looking
+in picking.
+
+
+"""
+import logging
+_logger = logging.getLogger(__name__)
+
+from openerp.osv import orm, fields
+from openerp import netsvc
+
+
+class sale_order_line(orm.Model):
+ _inherit = 'sale.order.line'
+
+ def field_qty_invoiced(self, cr, uid, ids, fields, arg, context):
+ res = dict.fromkeys(ids, 0)
+ for line in self.browse(cr, uid, ids, context=context):
+ for invoice_line in line.invoice_lines:
+ if invoice_line.invoice_id.state != 'cancel':
+ res[line.id] += invoice_line.quantity # XXX uom !
+ return res
+
+ def field_qty_delivered(self, cr, uid, ids, fields, arg, context):
+ res = dict.fromkeys(ids, 0)
+ for line in self.browse(cr, uid, ids, context=context):
+ if not line.move_ids:
+ # consumable or service: assume delivered == invoiced
+ res[line.id] = line.qty_invoiced
+ else:
+ for move in line.move_ids:
+ if (move.state == 'done'
+ and move.picking_id
+ and move.picking_id.type == 'out'):
+ res[line.id] += move.product_qty
+ return res
+
+ def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
+ res = super(sale_order_line,
+ self)._prepare_order_line_invoice_line(cr,
+ uid,
+ line,
+ account_id,
+ context)
+ if '_partial_invoice' in context:
+ # we are making a partial invoice for the line
+ to_invoice_qty = context['_partial_invoice'][line.id]
+ else:
+ # we are invoicing the yet uninvoiced part of the line
+ to_invoice_qty = line.product_uom_qty - line.qty_invoiced
+ res['quantity'] = to_invoice_qty
+ return res
+
+ def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
+ res = dict.fromkeys(ids, False)
+ for this in self.browse(cr, uid, ids, context=context):
+ res[this.id] = (this.qty_invoiced == this.product_uom_qty)
+ return res
+
+ def _order_lines_from_invoice2(self, cr, uid, ids, context=None):
+ # overridden with different name because called by framework with
+ # 'self' an instance of another class
+ return self.pool['sale.order.line']._order_lines_from_invoice(cr, uid, ids, context)
+
+ _columns = {
+ 'qty_invoiced': fields.function(field_qty_invoiced,
+ string='Invoiced Quantity',
+ type='float',
+ help="the quantity of product from this line "
+ "already invoiced"),
+ 'qty_delivered': fields.function(field_qty_delivered,
+ string='Invoiced Quantity',
+ type='float',
+ help="the quantity of product from this line "
+ "already invoiced"),
+ 'invoiced': fields.function(_fnct_line_invoiced,
+ string='Invoiced',
+ type='boolean',
+ store={
+ 'account.invoice': (_order_lines_from_invoice2,
+ ['state'], 10),
+ 'sale.order.line': (
+ lambda self,cr,uid,ids,ctx=None: ids,
+ ['invoice_lines'], 10
+ )
+ }
+ ),
+ }
+
+
+class sale_order(orm.Model):
+ _inherit = 'sale.order'
+
+
+class sale_advance_payment_inv(orm.TransientModel):
+ _inherit = "sale.advance.payment.inv"
+
+ def create_invoices(self, cr, uid, ids, context=None):
+ """override standard behavior if payment method is set to 'lines':
+ """
+ res = super(sale_advance_payment_inv, self).create_invoices(cr, uid, ids, context)
+ wizard = self.browse(cr, uid, ids[0], context)
+ if wizard.advance_payment_method != 'lines':
+ return res
+ sale_ids = context.get('active_ids', [])
+ if not sale_ids:
+ return res
+ wizard_obj = self.pool['sale.order.line.invoice.partially']
+ order_line_obj = self.pool['sale.order.line']
+ so_domain = [('order_id', 'in', sale_ids),]
+ so_line_ids = order_line_obj.search(cr, uid, so_domain, context=context)
+ line_values = []
+ for so_line in order_line_obj.browse(cr, uid, so_line_ids, context=context):
+ if so_line.state in ('confirmed', 'done') and not so_line.invoiced:
+ val = {'sale_order_line_id': so_line.id,}
+ if so_line.product_id and so_line.product_id.type == 'product':
+ val['quantity'] = so_line.qty_delivered - so_line.qty_invoiced
+ else:
+ # service or consumable
+ val['quantity'] = so_line.product_uom_qty - so_line.qty_invoiced
+ line_values.append((0, 0, val))
+ val = {'line_ids': line_values,}
+ wizard_id = wizard_obj.create(cr, uid, val, context=context)
+ wiz = wizard_obj.browse(cr, uid, wizard_id, context=context)
+ print wiz.line_ids
+ res = {'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'sale.order.line.invoice.partially',
+ 'res_id': wizard_id,
+ 'type': 'ir.actions.act_window',
+ 'target': 'new',
+ 'context': context,
+ }
+ return res
+
+class sale_order_line_invoice_partially_line(orm.TransientModel):
+ _name = "sale.order.line.invoice.partially.line"
+ _columns = {
+ 'wizard_id': fields.many2one('sale.order.line.invoice.partially',
+ string='Wizard'),
+ 'sale_order_line_id': fields.many2one('sale.order.line',
+ string='sale.order.line'),
+ 'name': fields.related('sale_order_line_id', 'name',
+ type='char', string="Line"),
+ 'order_qty': fields.related('sale_order_line_id', 'product_uom_qty',
+ type='float', string="Sold"),
+ 'qty_invoiced': fields.related('sale_order_line_id', 'qty_invoiced',
+ type='float', string="Invoiced"),
+ 'qty_delivered': fields.related('sale_order_line_id', 'qty_delivered',
+ type='float', string="Shipped"),
+ 'quantity': fields.float('To invoice'),
+ }
+
+
+class sale_order_line_invoice_partially(orm.TransientModel):
+ _name = "sale.order.line.invoice.partially"
+ _columns = {
+ 'name': fields.char('Name'),
+ 'line_ids': fields.one2many('sale.order.line.invoice.partially.line',
+ 'wizard_id', string="Lines"),
+ }
+
+ def create_invoice(self, cr, uid, ids, context=None):
+ wf_service = netsvc.LocalService('workflow')
+ if context is None:
+ context = {}
+ ctx = context.copy()
+ ctx['_partial_invoice'] = {}
+ so_line_obj = self.pool['sale.order.line']
+ so_obj = self.pool['sale.order']
+ order_lines = {}
+ for wiz in self.browse(cr, uid, ids, context=context):
+ for line in wiz.line_ids:
+ if line.quantity == 0:
+ continue
+ sale_order = line.sale_order_line_id.order_id
+ if sale_order.id not in order_lines:
+ order_lines[sale_order.id] = []
+ order_lines[sale_order.id].append(line.sale_order_line_id.id)
+ ctx['_partial_invoice'][line.sale_order_line_id.id] = line.quantity
+ for order_id in order_lines:
+ line_ids = order_lines[order_id]
+ invoice_line_ids = so_line_obj.invoice_line_create(cr,
+ uid,
+ line_ids,
+ context=ctx)
+ order = so_obj.browse(cr, uid, order_id, context=context)
+ invoice_id = so_obj._make_invoice(cr, uid,
+ order,
+ invoice_line_ids,
+ context=ctx)
+ _logger.info('created invoice %d', invoice_id)
+ # the following is copied from many places around
+ # (actually sale_line_invoice.py)
+ cr.execute('INSERT INTO sale_order_invoice_rel (order_id, '
+ ' invoice_id) '
+ 'VALUES (%s,%s)', (order_id, invoice_id))
+ if all(line.invoiced for line in order.order_line):
+ wf_service.trg_validate(uid, 'sale.order', order.id, 'manual_invoice', cr)
+ return {'type': 'ir.actions.act_window_close'}
=== added file 'sale_order_partial_invoice/sale_view.xml'
--- sale_order_partial_invoice/sale_view.xml 1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/sale_view.xml 2013-12-17 13:52:28 +0000
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+
+ <record id="view_order_line_tree" model="ir.ui.view">
+ <field name="name">sale.order.line.tree</field>
+ <field name="model">sale.order.line</field>
+ <field name="inherit_id" ref="sale.view_order_line_tree"/>
+ <field name="arch" type="xml">
+ <field name="product_uom_qty" position="after">
+ <field name="qty_invoiced" string="Inv. Qty"/>
+ <field name="qty_delivered" string="Shipped Qty"/>
+ </field>
+ </field>
+ </record>
+
+ <record id="view_order_line_form2" model="ir.ui.view">
+ <field name="name">sale.order.line.form2</field>
+ <field name="model">sale.order.line</field>
+ <field name="inherit_id" ref="sale.view_order_line_form2"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='product_uom_qty']/.." position="after">
+ <field name="qty_invoiced" />
+ <field name="qty_delivered"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_sale_order_invoice_partially_form" model="ir.ui.view">
+ <field name="name">sale.order.line.invoice.partially.form</field>
+ <field name="model">sale.order.line.invoice.partially</field>
+ <field name="arch" type="xml">
+ <form version="7.0" string="Invoice Sale Order Lines">
+ <sheet>
+ <separator string="Invoice lines"/>
+ <group>
+ <field name="line_ids" nolabel="1">
+ <tree version="7.0" string="Lines" create="false" editable="bottom">
+ <field name="sale_order_line_id" invisible="1"/>
+ <field name="name"/>
+ <field name="order_qty"/>
+ <field name="qty_invoiced"/>
+ <field name="qty_delivered"/>
+ <field name="quantity"/>
+ </tree>
+ </field>
+ </group>
+ </sheet>
+ <footer>
+ <button name="create_invoice" type="object" string="Create Invoice" class="oe_highlight"/>
+ or
+ <button string="Cancel" class="oe_link" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ </data>
+</openerp>
=== added directory 'sale_order_partial_invoice/test'
=== added file 'sale_order_partial_invoice/test/make_remaining_invoice.yml'
--- sale_order_partial_invoice/test/make_remaining_invoice.yml 1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/test/make_remaining_invoice.yml 2013-12-17 13:52:28 +0000
@@ -0,0 +1,169 @@
+-
+ Create stockable products for testing purpose
+-
+ !record {model: product.product, id: product_stockable}:
+ name: 'LCD Projector'
+ list_price: 1234.99
+ type: product
+ procure_method: 'make_to_stock'
+-
+ !record {model: product.product, id: product_service}:
+ name: 'LCD Projector 3 Year Warranty'
+ list_price: 299.99
+ type: service
+ procure_method: 'make_to_stock'
+-
+ !record {model: product.product, id: product_consumable}:
+ name: 'Spare screws'
+ list_price: 1.22
+ type: consu
+ procure_method: 'make_to_stock'
+-
+ Create stock for the stockable product
+-
+ !record {model: stock.move, id: move1}:
+ location_id: stock.stock_location_suppliers
+ location_dest_id: stock.stock_location_stock
+ product_id: product_stockable
+ product_qty: 5
+ state: done
+-
+ Create a sale order for the above
+-
+ !record {model: sale.order, id: sale_order2}:
+ partner_id: base.res_partner_2
+ order_policy: manual
+ order_line:
+ - product_id: product_stockable
+ product_uom_qty: 3
+ - product_id: product_service
+ product_uom_qty: 3
+ - product_id: product_consumable
+ product_uom_qty: 12
+-
+ Confirm Order
+-
+ !workflow {model: sale.order, action: order_confirm, ref: sale_order2}
+-
+ Partial delivery
+-
+ !python {model: stock.picking}: |
+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2"))])
+ first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+ if first_picking.force_assign(cr, uid, first_picking):
+ assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
+ partial_moves = {}
+ for move in first_picking.move_lines:
+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
+ first_picking.do_partial(partial_moves, context=context)
+-
+ Create partial invoice
+-
+ !python {model: sale.order.line.invoice.partially}: |
+ sale_obj = self.pool['sale.order']
+ sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order2"), context=context)
+ wiz_data = {'name': 'wiz1',
+ 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
+ (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
+ (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
+ ],
+ }
+ id = self.create(cr, uid, wiz_data, context=context)
+ self.create_invoice(cr, uid, [id], context=context)
+-
+ Check the 1st invoice is created
+-
+ !python {model: sale.order}: |
+ order = self.browse(cr, uid, ref('sale_order2'))
+ assert len(order.invoice_ids) == 1, "I should have 1 invoice"
+-
+ Check qty_invoiced 4
+-
+ !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 1
+ - order_line[1].qty_invoiced == 1
+ - order_line[2].qty_invoiced == 4
+-
+ Check qty_delivered 4
+-
+ !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 1
+ - order_line[1].qty_delivered == 1
+ - order_line[2].qty_delivered == 4
+-
+ Partial delivery 2
+-
+ !python {model: stock.picking}: |
+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2")), ('state', '!=', 'done')])
+ picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+ if picking.force_assign(cr, uid, picking):
+ assert len(picking.move_lines) == 2, "wrong count of stock moves"
+ partial_moves = {}
+ for move in picking.move_lines:
+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
+ picking.do_partial(partial_moves, context=context)
+-
+ I invoice the remaining of the SO
+-
+ !python {model: sale.advance.payment.inv}: |
+ ctx = context.copy()
+ ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order2")], "active_id":ref("sale_order2")})
+ pay_id = self.create(cr, uid, {'advance_payment_method': 'all'})
+ self.create_invoices(cr, uid, [pay_id], context=ctx)
+-
+ Check the invoiced qtys in the invoice
+-
+ !python {model: sale.order}: |
+ order = self.browse(cr, uid, ref('sale_order2'))
+ assert len(order.invoice_ids) == 2, "I should have 2 invoices"
+-
+ Check qty_invoiced 5
+-
+ !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 3
+ - order_line[1].qty_invoiced == 3
+ - order_line[2].qty_invoiced == 12
+-
+ Check qty_delivered 5
+-
+ !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 3
+ - order_line[1].qty_delivered == 3
+ - order_line[2].qty_delivered == 12
+-
+ I confirm the invoices
+-
+ !python {model: sale.order}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ so = self.browse(cr, uid, ref("sale_order2"))
+ for invoice in so.invoice_ids:
+ wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
+-
+ I pay the invoices.
+-
+ !python {model: account.invoice}: |
+ sale_order = self.pool.get('sale.order')
+ order = sale_order.browse(cr, uid, ref("sale_order2"))
+ journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
+ for invoice in order.invoice_ids:
+ invoice.pay_and_reconcile(
+ invoice.amount_total, ref('account.cash'), ref('account.period_8'),
+ journal_ids[0], ref('account.cash'),
+ ref('account.period_8'), journal_ids[0],
+ name='test')
+-
+ I run the scheduler (required to get the correct sale order state)
+-
+ !python {model: procurement.order}: |
+ self.run_scheduler(cr, uid)
+-
+ I check sale order
+-
+ !python {model: sale.order}: |
+ sale_order = self.browse(cr, uid, ref("sale_order2"))
+ assert sale_order.invoice_ids, "Invoice should be created."
+ assert sale_order.invoice_exists, "Order is not invoiced."
+ assert sale_order.invoiced, "Order is not paid."
+ assert sale_order.state == 'done', 'Order should be Done.'
+
=== added file 'sale_order_partial_invoice/test/partial_so_invoice.yml'
--- sale_order_partial_invoice/test/partial_so_invoice.yml 1970-01-01 00:00:00 +0000
+++ sale_order_partial_invoice/test/partial_so_invoice.yml 2013-12-17 13:52:28 +0000
@@ -0,0 +1,231 @@
+-
+ Create stockable products for testing purpose
+-
+ !record {model: product.product, id: product_stockable}:
+ name: 'LCD Projector'
+ list_price: 1234.99
+ type: product
+ procure_method: 'make_to_stock'
+-
+ !record {model: product.product, id: product_service}:
+ name: 'LCD Projector 3 Year Warranty'
+ list_price: 299.99
+ type: service
+ procure_method: 'make_to_stock'
+-
+ !record {model: product.product, id: product_consumable}:
+ name: 'Spare screws'
+ list_price: 1.22
+ type: consu
+ procure_method: 'make_to_stock'
+-
+ Create stock for the stockable product
+-
+ !record {model: stock.move, id: move1}:
+ location_id: stock.stock_location_suppliers
+ location_dest_id: stock.stock_location_stock
+ product_id: product_stockable
+ product_qty: 5
+ state: done
+-
+ Create a sale order for the above
+-
+ !record {model: sale.order, id: sale_order1}:
+ partner_id: base.res_partner_2
+ order_policy: manual
+ order_line:
+ - product_id: product_stockable
+ product_uom_qty: 3
+ - product_id: product_service
+ product_uom_qty: 3
+ - product_id: product_consumable
+ product_uom_qty: 12
+-
+ Check qty_invoiced 1
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 0
+ - order_line[1].qty_invoiced == 0
+ - order_line[2].qty_invoiced == 0
+-
+ Check qty_delivered 1
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 0
+ - order_line[1].qty_delivered == 0
+ - order_line[2].qty_delivered == 0
+-
+ Confirm Order
+-
+ !workflow {model: sale.order, action: order_confirm, ref: sale_order1}
+-
+ Check qty_invoiced 2
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 0
+ - order_line[1].qty_invoiced == 0
+ - order_line[2].qty_invoiced == 0
+-
+ Check qty_delivered 2
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 0
+ - order_line[1].qty_delivered == 0
+ - order_line[2].qty_delivered == 0
+-
+ Partial delivery
+-
+ !python {model: stock.picking}: |
+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1"))])
+ first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+ if first_picking.force_assign(cr, uid, first_picking):
+ assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
+ partial_moves = {}
+ for move in first_picking.move_lines:
+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
+ first_picking.do_partial(partial_moves, context=context)
+-
+ Check qty_invoiced 3
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 0
+ - order_line[1].qty_invoiced == 0
+ - order_line[2].qty_invoiced == 0
+-
+ Check qty_delivered 3
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 1
+ - order_line[1].qty_delivered == 0
+ - order_line[2].qty_delivered == 4
+-
+ Check SO state
+-
+ !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
+ - state == 'manual'
+-
+ Create partial invoice
+-
+ !python {model: sale.order.line.invoice.partially}: |
+ sale_obj = self.pool['sale.order']
+ sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order1"), context=context)
+ wiz_data = {'name': 'wiz1',
+ 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
+ (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
+ (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
+ ],
+ }
+ id = self.create(cr, uid, wiz_data, context=context)
+ self.create_invoice(cr, uid, [id], context=context)
+-
+ Check the 1st invoice is created
+-
+ !python {model: sale.order}: |
+ order = self.browse(cr, uid, ref('sale_order1'))
+ assert len(order.invoice_ids) == 1, "I should have 1 invoice"
+-
+ Check qty_invoiced 4
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 1
+ - order_line[1].qty_invoiced == 1
+ - order_line[2].qty_invoiced == 4
+-
+ Check qty_delivered 4
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 1
+ - order_line[1].qty_delivered == 1
+ - order_line[2].qty_delivered == 4
+-
+ Check SO state
+-
+ !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
+ - state == 'manual'
+-
+ Partial delivery 2
+-
+ !python {model: stock.picking}: |
+ delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1")), ('state', '!=', 'done')])
+ picking = self.browse(cr, uid, delivery_orders[-1], context=context)
+ if picking.force_assign(cr, uid, picking):
+ assert len(picking.move_lines) == 2, "wrong count of stock moves"
+ partial_moves = {}
+ for move in picking.move_lines:
+ partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
+ picking.do_partial(partial_moves, context=context)
+-
+ Create partial invoice 2
+-
+ !python {model: sale.order.line.invoice.partially}: |
+ sale_obj = self.pool['sale.order']
+ sale = sale_obj.browse(cr, uid, ref("sale_order1"), context=context)
+ wiz_data = {'name': 'wiz2',
+ 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 2}),
+ (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 2}),
+ (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 8}),
+ ],
+ }
+ id = self.create(cr, uid, wiz_data, context=context)
+ self.create_invoice(cr, uid, [id], context=context)
+-
+ Check the 2nd invoice is created
+-
+ !python {model: sale.order}: |
+ order = self.browse(cr, uid, ref('sale_order1'))
+ assert len(order.invoice_ids) == 2, "I should have 2 invoices"
+-
+ Check qty_invoiced 5
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
+ - order_line[0].qty_invoiced == 3
+ - order_line[1].qty_invoiced == 3
+ - order_line[2].qty_invoiced == 12
+-
+ Check qty_delivered 5
+-
+ !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
+ - order_line[0].qty_delivered == 3
+ - order_line[1].qty_delivered == 3
+ - order_line[2].qty_delivered == 12
+-
+ Check SO state
+-
+ !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
+ - state == 'manual'
+-
+ I confirm the invoices
+-
+ !python {model: sale.order}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ so = self.browse(cr, uid, ref("sale_order1"))
+ for invoice in so.invoice_ids:
+ wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
+-
+ I pay the invoices.
+-
+ !python {model: account.invoice}: |
+ sale_order = self.pool.get('sale.order')
+ order = sale_order.browse(cr, uid, ref("sale_order1"))
+ journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
+ for invoice in order.invoice_ids:
+ invoice.pay_and_reconcile(
+ invoice.amount_total, ref('account.cash'), ref('account.period_8'),
+ journal_ids[0], ref('account.cash'),
+ ref('account.period_8'), journal_ids[0],
+ name='test')
+-
+ I run the scheduler (required to get the correct sale order state)
+-
+ !python {model: procurement.order}: |
+ self.run_scheduler(cr, uid)
+-
+ I check sale order
+-
+ !python {model: sale.order}: |
+ sale_order = self.browse(cr, uid, ref("sale_order1"))
+ assert sale_order.invoice_ids, "Invoice should be created."
+ assert sale_order.invoice_exists, "Order is not invoiced."
+ assert sale_order.invoiced, "Order is not paid."
+ assert sale_order.state == 'done', 'Order should be Done.'
References