Merge lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe into lp:~account-core-editors/account-invoicing/7.0

Proposed by Alexandre Fayolle - camptocamp
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 30
Merged at revision: 26
Proposed branch: lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe
Merge into: lp:~account-core-editors/account-invoicing/7.0
Diff against target: 805 lines (+773/-0)
6 files modified
sale_order_partial_invoice/__init__.py (+23/-0)
sale_order_partial_invoice/__openerp__.py (+51/-0)
sale_order_partial_invoice/sale.py (+240/-0)
sale_order_partial_invoice/sale_view.xml (+59/-0)
sale_order_partial_invoice/test/make_remaining_invoice.yml (+169/-0)
sale_order_partial_invoice/test/partial_so_invoice.yml (+231/-0)
To merge this branch: bzr merge lp:~camptocamp/account-invoicing/7.0-add-sale_order_partial_invoice-afe
Reviewer Review Type Date Requested Status
Joël Grand-Guillaume @ camptocamp code review + tests Needs Information
Nicolas Bessi - Camptocamp (community) no test, code review Approve
Leonardo Pistone code review Approve
Review via email: mp+199282@code.launchpad.net

Description of the change

[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

To post a comment you must log in.
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

To use it / test it:

* create a SO with several lines (products and services can be used),
with "manual" (On Demand) invoicing policy
* deliver (partially or completely)
* click "Create Invoice" and select "Invoice some order lines"

A wizard is displayed instead of the List view of sale order line, and
it is possible in that wizard to change the quantity which will appear
in the invoice. The already invoiced and delivered quantities are also
displayed (for services the delivered quantity is equal to the invoiced
quantity).

If a partial invoice was generated, generating a new invoice for the
whole Sale Order takes the partial invoice into account.

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Hello,

invoice.py is never imported.
Confirmed with Alexandre, dead code I will fix it.
Put MP in workin progress unitl fix commited.

Regards

Nicolas

Revision history for this message
Leonardo Pistone (lepistone) wrote :

Thanks for your contribution Alexandre!

* some pprints around
* a header mentions Tiny

review: Needs Fixing
27. By Nicolas Bessi - Camptocamp

[FIX] forgotten pprint

28. By Nicolas Bessi - Camptocamp

[FIX] unused import of _

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

class sale_order(orm.Model):
    _inherit = 'sale.order'

Dead code ??

review: Needs Information
Revision history for this message
Leonardo Pistone (lepistone) wrote :

Since Nicolas has fixed what I pointed out, I change my review.

Thanks!

review: Approve (code review)
29. By Nicolas Bessi - Camptocamp

[FIX] PEP8 condition operator at end (personal taste)

30. By Nicolas Bessi - Camptocamp

[FIX] unused initialized class

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Just committed small neat pick.

LGTM, but it would be nice if Alexandre can just redo a run of test with last fixes.

Regards

Nicolas

review: Approve (no test, code review)
Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi Alexandre,

This contrib, really rocks ! A question:

 * When displaying the lines and quantities to invoice (with Line;Sold;Ivoiced;Shipped;To invoice ), I didn't understand why fields are not in read-only except "To invoice" one ? Any reason not to let the user change other field ?

A part from that, it looked perfect to me, it was just like this that I expected this to work, thanks.

Regards,

Joël

review: Needs Information (code review + tests)
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

> Hi Alexandre,
>
>
> This contrib, really rocks ! A question:
>
> * When displaying the lines and quantities to invoice (with
> Line;Sold;Ivoiced;Shipped;To invoice ), I didn't understand why fields are not
> in read-only except "To invoice" one ? Any reason not to let the user change
> other field ?

You are right, I'll make the required changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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 14:13:31 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Alexandre Fayolle
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import sale
22
23# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
024
=== 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 14:13:31 +0000
@@ -0,0 +1,51 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Alexandre Fayolle
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22{
23 'name': 'Sale Partial Invoice',
24 'version': '1.1',
25 'category': 'Accounting & Finance',
26 'description': """Allow to partialy invoice Sale Order lines
27
28 With a sale order in 'manual' invoicing policy, when the user selects to
29 invoice some SO lines, a new wizard is display in which it is possible to
30 select how much of the different lines is to be invoiced. The amounts
31 invoiced and the amounts delivered are also displayed. When generating an
32 invoice for the whole sale order, the partial invoices are taken into
33 account and only the amounts not already invoiced are part of the new
34 invoice
35 """,
36 'author': 'Camptocamp',
37 'website': 'http://www.camptocamp.com',
38 'license': 'AGPL-3',
39 'depends': ['sale', 'account', 'sale_stock'],
40 'data': [
41 'sale_view.xml',
42 ],
43 'test': [
44 'test/partial_so_invoice.yml',
45 'test/make_remaining_invoice.yml',
46 ],
47 'demo': [],
48 'installable': True,
49 'active': False,
50}
51# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
052
=== 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 14:13:31 +0000
@@ -0,0 +1,240 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Alexandre Fayolle
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21"""
22 * Adding qty_invoiced field on SO lines, computed based on invoice lines
23 linked to it that has the same product. So this way, advance invoice will
24 still work !
25
26 * Adding qty_delivered field in SO Lines, computed from move lines linked to
27 it. For services, the quantity delivered is a problem, the MRP will
28 automatically run the procurement linked to this line and pass it to done. I
29 suggest that in that case, delivered qty = invoiced_qty as the procurement
30 is for the whole qty, it'll be a good alternative to follow what has been
31 done and not.
32
33 * Add in the "Order Line to invoice" view those fields
34
35 * Change the behavior of the "invoiced" field of the SO line to be true when
36 all is invoiced
37
38 * Adapt the "_make_invoice" method in SO to deal with qty_invoiced
39
40 * Adapt the sale_line_invoice.py wizard to deal with qty_invoiced, asking the
41 user how much he want to invoice.
42
43By having the delivered quantity, we can imagine in the future to provide an
44invoicing "based on delivery" that will look at those values instead of looking
45in picking.
46
47
48"""
49import logging
50_logger = logging.getLogger(__name__)
51
52from openerp.osv import orm, fields
53from openerp import netsvc
54
55
56class sale_order_line(orm.Model):
57 _inherit = 'sale.order.line'
58
59 def field_qty_invoiced(self, cr, uid, ids, fields, arg, context):
60 res = dict.fromkeys(ids, 0)
61 for line in self.browse(cr, uid, ids, context=context):
62 for invoice_line in line.invoice_lines:
63 if invoice_line.invoice_id.state != 'cancel':
64 res[line.id] += invoice_line.quantity # XXX uom !
65 return res
66
67 def field_qty_delivered(self, cr, uid, ids, fields, arg, context):
68 res = dict.fromkeys(ids, 0)
69 for line in self.browse(cr, uid, ids, context=context):
70 if not line.move_ids:
71 # consumable or service: assume delivered == invoiced
72 res[line.id] = line.qty_invoiced
73 else:
74 for move in line.move_ids:
75 if (move.state == 'done' and
76 move.picking_id and
77 move.picking_id.type == 'out'):
78 res[line.id] += move.product_qty
79 return res
80
81 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
82 res = super(sale_order_line,
83 self)._prepare_order_line_invoice_line(cr,
84 uid,
85 line,
86 account_id,
87 context)
88 if '_partial_invoice' in context:
89 # we are making a partial invoice for the line
90 to_invoice_qty = context['_partial_invoice'][line.id]
91 else:
92 # we are invoicing the yet uninvoiced part of the line
93 to_invoice_qty = line.product_uom_qty - line.qty_invoiced
94 res['quantity'] = to_invoice_qty
95 return res
96
97 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
98 res = dict.fromkeys(ids, False)
99 for this in self.browse(cr, uid, ids, context=context):
100 res[this.id] = (this.qty_invoiced == this.product_uom_qty)
101 return res
102
103 def _order_lines_from_invoice2(self, cr, uid, ids, context=None):
104 # overridden with different name because called by framework with
105 # 'self' an instance of another class
106 return self.pool['sale.order.line']._order_lines_from_invoice(cr, uid, ids, context)
107
108 _columns = {
109 'qty_invoiced': fields.function(field_qty_invoiced,
110 string='Invoiced Quantity',
111 type='float',
112 help="the quantity of product from this line "
113 "already invoiced"),
114 'qty_delivered': fields.function(field_qty_delivered,
115 string='Invoiced Quantity',
116 type='float',
117 help="the quantity of product from this line "
118 "already invoiced"),
119 'invoiced': fields.function(_fnct_line_invoiced,
120 string='Invoiced',
121 type='boolean',
122 store={
123 'account.invoice': (_order_lines_from_invoice2,
124 ['state'], 10),
125 'sale.order.line': (
126 lambda self,cr,uid,ids,ctx=None: ids,
127 ['invoice_lines'], 10
128 )
129 }
130 ),
131 }
132
133
134class sale_advance_payment_inv(orm.TransientModel):
135 _inherit = "sale.advance.payment.inv"
136
137 def create_invoices(self, cr, uid, ids, context=None):
138 """override standard behavior if payment method is set to 'lines':
139 """
140 res = super(sale_advance_payment_inv, self).create_invoices(cr, uid, ids, context)
141 wizard = self.browse(cr, uid, ids[0], context)
142 if wizard.advance_payment_method != 'lines':
143 return res
144 sale_ids = context.get('active_ids', [])
145 if not sale_ids:
146 return res
147 wizard_obj = self.pool['sale.order.line.invoice.partially']
148 order_line_obj = self.pool['sale.order.line']
149 so_domain = [('order_id', 'in', sale_ids),]
150 so_line_ids = order_line_obj.search(cr, uid, so_domain, context=context)
151 line_values = []
152 for so_line in order_line_obj.browse(cr, uid, so_line_ids, context=context):
153 if so_line.state in ('confirmed', 'done') and not so_line.invoiced:
154 val = {'sale_order_line_id': so_line.id,}
155 if so_line.product_id and so_line.product_id.type == 'product':
156 val['quantity'] = so_line.qty_delivered - so_line.qty_invoiced
157 else:
158 # service or consumable
159 val['quantity'] = so_line.product_uom_qty - so_line.qty_invoiced
160 line_values.append((0, 0, val))
161 val = {'line_ids': line_values,}
162 wizard_id = wizard_obj.create(cr, uid, val, context=context)
163 wiz = wizard_obj.browse(cr, uid, wizard_id, context=context)
164 print wiz.line_ids
165 res = {'view_type': 'form',
166 'view_mode': 'form',
167 'res_model': 'sale.order.line.invoice.partially',
168 'res_id': wizard_id,
169 'type': 'ir.actions.act_window',
170 'target': 'new',
171 'context': context,
172 }
173 return res
174
175
176class sale_order_line_invoice_partially_line(orm.TransientModel):
177 _name = "sale.order.line.invoice.partially.line"
178 _columns = {
179 'wizard_id': fields.many2one('sale.order.line.invoice.partially',
180 string='Wizard'),
181 'sale_order_line_id': fields.many2one('sale.order.line',
182 string='sale.order.line'),
183 'name': fields.related('sale_order_line_id', 'name',
184 type='char', string="Line"),
185 'order_qty': fields.related('sale_order_line_id', 'product_uom_qty',
186 type='float', string="Sold"),
187 'qty_invoiced': fields.related('sale_order_line_id', 'qty_invoiced',
188 type='float', string="Invoiced"),
189 'qty_delivered': fields.related('sale_order_line_id', 'qty_delivered',
190 type='float', string="Shipped"),
191 'quantity': fields.float('To invoice'),
192 }
193
194
195class sale_order_line_invoice_partially(orm.TransientModel):
196 _name = "sale.order.line.invoice.partially"
197 _columns = {
198 'name': fields.char('Name'),
199 'line_ids': fields.one2many('sale.order.line.invoice.partially.line',
200 'wizard_id', string="Lines"),
201 }
202
203 def create_invoice(self, cr, uid, ids, context=None):
204 wf_service = netsvc.LocalService('workflow')
205 if context is None:
206 context = {}
207 ctx = context.copy()
208 ctx['_partial_invoice'] = {}
209 so_line_obj = self.pool['sale.order.line']
210 so_obj = self.pool['sale.order']
211 order_lines = {}
212 for wiz in self.browse(cr, uid, ids, context=context):
213 for line in wiz.line_ids:
214 if line.quantity == 0:
215 continue
216 sale_order = line.sale_order_line_id.order_id
217 if sale_order.id not in order_lines:
218 order_lines[sale_order.id] = []
219 order_lines[sale_order.id].append(line.sale_order_line_id.id)
220 ctx['_partial_invoice'][line.sale_order_line_id.id] = line.quantity
221 for order_id in order_lines:
222 line_ids = order_lines[order_id]
223 invoice_line_ids = so_line_obj.invoice_line_create(cr,
224 uid,
225 line_ids,
226 context=ctx)
227 order = so_obj.browse(cr, uid, order_id, context=context)
228 invoice_id = so_obj._make_invoice(cr, uid,
229 order,
230 invoice_line_ids,
231 context=ctx)
232 _logger.info('created invoice %d', invoice_id)
233 # the following is copied from many places around
234 # (actually sale_line_invoice.py)
235 cr.execute('INSERT INTO sale_order_invoice_rel (order_id, '
236 ' invoice_id) '
237 'VALUES (%s,%s)', (order_id, invoice_id))
238 if all(line.invoiced for line in order.order_line):
239 wf_service.trg_validate(uid, 'sale.order', order.id, 'manual_invoice', cr)
240 return {'type': 'ir.actions.act_window_close'}
0241
=== 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 14:13:31 +0000
@@ -0,0 +1,59 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4
5 <record id="view_order_line_tree" model="ir.ui.view">
6 <field name="name">sale.order.line.tree</field>
7 <field name="model">sale.order.line</field>
8 <field name="inherit_id" ref="sale.view_order_line_tree"/>
9 <field name="arch" type="xml">
10 <field name="product_uom_qty" position="after">
11 <field name="qty_invoiced" string="Inv. Qty"/>
12 <field name="qty_delivered" string="Shipped Qty"/>
13 </field>
14 </field>
15 </record>
16
17 <record id="view_order_line_form2" model="ir.ui.view">
18 <field name="name">sale.order.line.form2</field>
19 <field name="model">sale.order.line</field>
20 <field name="inherit_id" ref="sale.view_order_line_form2"/>
21 <field name="arch" type="xml">
22 <xpath expr="//field[@name='product_uom_qty']/.." position="after">
23 <field name="qty_invoiced" />
24 <field name="qty_delivered"/>
25 </xpath>
26 </field>
27 </record>
28
29 <record id="view_sale_order_invoice_partially_form" model="ir.ui.view">
30 <field name="name">sale.order.line.invoice.partially.form</field>
31 <field name="model">sale.order.line.invoice.partially</field>
32 <field name="arch" type="xml">
33 <form version="7.0" string="Invoice Sale Order Lines">
34 <sheet>
35 <separator string="Invoice lines"/>
36 <group>
37 <field name="line_ids" nolabel="1">
38 <tree version="7.0" string="Lines" create="false" editable="bottom">
39 <field name="sale_order_line_id" invisible="1"/>
40 <field name="name"/>
41 <field name="order_qty"/>
42 <field name="qty_invoiced"/>
43 <field name="qty_delivered"/>
44 <field name="quantity"/>
45 </tree>
46 </field>
47 </group>
48 </sheet>
49 <footer>
50 <button name="create_invoice" type="object" string="Create Invoice" class="oe_highlight"/>
51 or
52 <button string="Cancel" class="oe_link" special="cancel" />
53 </footer>
54 </form>
55 </field>
56 </record>
57
58 </data>
59</openerp>
060
=== 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 14:13:31 +0000
@@ -0,0 +1,169 @@
1-
2 Create stockable products for testing purpose
3-
4 !record {model: product.product, id: product_stockable}:
5 name: 'LCD Projector'
6 list_price: 1234.99
7 type: product
8 procure_method: 'make_to_stock'
9-
10 !record {model: product.product, id: product_service}:
11 name: 'LCD Projector 3 Year Warranty'
12 list_price: 299.99
13 type: service
14 procure_method: 'make_to_stock'
15-
16 !record {model: product.product, id: product_consumable}:
17 name: 'Spare screws'
18 list_price: 1.22
19 type: consu
20 procure_method: 'make_to_stock'
21-
22 Create stock for the stockable product
23-
24 !record {model: stock.move, id: move1}:
25 location_id: stock.stock_location_suppliers
26 location_dest_id: stock.stock_location_stock
27 product_id: product_stockable
28 product_qty: 5
29 state: done
30-
31 Create a sale order for the above
32-
33 !record {model: sale.order, id: sale_order2}:
34 partner_id: base.res_partner_2
35 order_policy: manual
36 order_line:
37 - product_id: product_stockable
38 product_uom_qty: 3
39 - product_id: product_service
40 product_uom_qty: 3
41 - product_id: product_consumable
42 product_uom_qty: 12
43-
44 Confirm Order
45-
46 !workflow {model: sale.order, action: order_confirm, ref: sale_order2}
47-
48 Partial delivery
49-
50 !python {model: stock.picking}: |
51 delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2"))])
52 first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
53 if first_picking.force_assign(cr, uid, first_picking):
54 assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
55 partial_moves = {}
56 for move in first_picking.move_lines:
57 partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
58 first_picking.do_partial(partial_moves, context=context)
59-
60 Create partial invoice
61-
62 !python {model: sale.order.line.invoice.partially}: |
63 sale_obj = self.pool['sale.order']
64 sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order2"), context=context)
65 wiz_data = {'name': 'wiz1',
66 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
67 (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
68 (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
69 ],
70 }
71 id = self.create(cr, uid, wiz_data, context=context)
72 self.create_invoice(cr, uid, [id], context=context)
73-
74 Check the 1st invoice is created
75-
76 !python {model: sale.order}: |
77 order = self.browse(cr, uid, ref('sale_order2'))
78 assert len(order.invoice_ids) == 1, "I should have 1 invoice"
79-
80 Check qty_invoiced 4
81-
82 !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
83 - order_line[0].qty_invoiced == 1
84 - order_line[1].qty_invoiced == 1
85 - order_line[2].qty_invoiced == 4
86-
87 Check qty_delivered 4
88-
89 !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
90 - order_line[0].qty_delivered == 1
91 - order_line[1].qty_delivered == 1
92 - order_line[2].qty_delivered == 4
93-
94 Partial delivery 2
95-
96 !python {model: stock.picking}: |
97 delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order2")), ('state', '!=', 'done')])
98 picking = self.browse(cr, uid, delivery_orders[-1], context=context)
99 if picking.force_assign(cr, uid, picking):
100 assert len(picking.move_lines) == 2, "wrong count of stock moves"
101 partial_moves = {}
102 for move in picking.move_lines:
103 partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
104 picking.do_partial(partial_moves, context=context)
105-
106 I invoice the remaining of the SO
107-
108 !python {model: sale.advance.payment.inv}: |
109 ctx = context.copy()
110 ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order2")], "active_id":ref("sale_order2")})
111 pay_id = self.create(cr, uid, {'advance_payment_method': 'all'})
112 self.create_invoices(cr, uid, [pay_id], context=ctx)
113-
114 Check the invoiced qtys in the invoice
115-
116 !python {model: sale.order}: |
117 order = self.browse(cr, uid, ref('sale_order2'))
118 assert len(order.invoice_ids) == 2, "I should have 2 invoices"
119-
120 Check qty_invoiced 5
121-
122 !assert {model: sale.order, id: sale_order2, string: qty_invoiced not correctly implemented}:
123 - order_line[0].qty_invoiced == 3
124 - order_line[1].qty_invoiced == 3
125 - order_line[2].qty_invoiced == 12
126-
127 Check qty_delivered 5
128-
129 !assert {model: sale.order, id: sale_order2, string: qty_delivered not correctly implemented}:
130 - order_line[0].qty_delivered == 3
131 - order_line[1].qty_delivered == 3
132 - order_line[2].qty_delivered == 12
133-
134 I confirm the invoices
135-
136 !python {model: sale.order}: |
137 import netsvc
138 wf_service = netsvc.LocalService("workflow")
139 so = self.browse(cr, uid, ref("sale_order2"))
140 for invoice in so.invoice_ids:
141 wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
142-
143 I pay the invoices.
144-
145 !python {model: account.invoice}: |
146 sale_order = self.pool.get('sale.order')
147 order = sale_order.browse(cr, uid, ref("sale_order2"))
148 journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
149 for invoice in order.invoice_ids:
150 invoice.pay_and_reconcile(
151 invoice.amount_total, ref('account.cash'), ref('account.period_8'),
152 journal_ids[0], ref('account.cash'),
153 ref('account.period_8'), journal_ids[0],
154 name='test')
155-
156 I run the scheduler (required to get the correct sale order state)
157-
158 !python {model: procurement.order}: |
159 self.run_scheduler(cr, uid)
160-
161 I check sale order
162-
163 !python {model: sale.order}: |
164 sale_order = self.browse(cr, uid, ref("sale_order2"))
165 assert sale_order.invoice_ids, "Invoice should be created."
166 assert sale_order.invoice_exists, "Order is not invoiced."
167 assert sale_order.invoiced, "Order is not paid."
168 assert sale_order.state == 'done', 'Order should be Done.'
169
0170
=== 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 14:13:31 +0000
@@ -0,0 +1,231 @@
1-
2 Create stockable products for testing purpose
3-
4 !record {model: product.product, id: product_stockable}:
5 name: 'LCD Projector'
6 list_price: 1234.99
7 type: product
8 procure_method: 'make_to_stock'
9-
10 !record {model: product.product, id: product_service}:
11 name: 'LCD Projector 3 Year Warranty'
12 list_price: 299.99
13 type: service
14 procure_method: 'make_to_stock'
15-
16 !record {model: product.product, id: product_consumable}:
17 name: 'Spare screws'
18 list_price: 1.22
19 type: consu
20 procure_method: 'make_to_stock'
21-
22 Create stock for the stockable product
23-
24 !record {model: stock.move, id: move1}:
25 location_id: stock.stock_location_suppliers
26 location_dest_id: stock.stock_location_stock
27 product_id: product_stockable
28 product_qty: 5
29 state: done
30-
31 Create a sale order for the above
32-
33 !record {model: sale.order, id: sale_order1}:
34 partner_id: base.res_partner_2
35 order_policy: manual
36 order_line:
37 - product_id: product_stockable
38 product_uom_qty: 3
39 - product_id: product_service
40 product_uom_qty: 3
41 - product_id: product_consumable
42 product_uom_qty: 12
43-
44 Check qty_invoiced 1
45-
46 !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
47 - order_line[0].qty_invoiced == 0
48 - order_line[1].qty_invoiced == 0
49 - order_line[2].qty_invoiced == 0
50-
51 Check qty_delivered 1
52-
53 !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
54 - order_line[0].qty_delivered == 0
55 - order_line[1].qty_delivered == 0
56 - order_line[2].qty_delivered == 0
57-
58 Confirm Order
59-
60 !workflow {model: sale.order, action: order_confirm, ref: sale_order1}
61-
62 Check qty_invoiced 2
63-
64 !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
65 - order_line[0].qty_invoiced == 0
66 - order_line[1].qty_invoiced == 0
67 - order_line[2].qty_invoiced == 0
68-
69 Check qty_delivered 2
70-
71 !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
72 - order_line[0].qty_delivered == 0
73 - order_line[1].qty_delivered == 0
74 - order_line[2].qty_delivered == 0
75-
76 Partial delivery
77-
78 !python {model: stock.picking}: |
79 delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1"))])
80 first_picking = self.browse(cr, uid, delivery_orders[-1], context=context)
81 if first_picking.force_assign(cr, uid, first_picking):
82 assert len(first_picking.move_lines) == 2, "wrong count of stock moves"
83 partial_moves = {}
84 for move in first_picking.move_lines:
85 partial_moves['move%s' % move.id] = {'product_qty': move.product_qty / 3, 'product_uom':ref('product.product_uom_unit')}
86 first_picking.do_partial(partial_moves, context=context)
87-
88 Check qty_invoiced 3
89-
90 !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
91 - order_line[0].qty_invoiced == 0
92 - order_line[1].qty_invoiced == 0
93 - order_line[2].qty_invoiced == 0
94-
95 Check qty_delivered 3
96-
97 !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
98 - order_line[0].qty_delivered == 1
99 - order_line[1].qty_delivered == 0
100 - order_line[2].qty_delivered == 4
101-
102 Check SO state
103-
104 !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
105 - state == 'manual'
106-
107 Create partial invoice
108-
109 !python {model: sale.order.line.invoice.partially}: |
110 sale_obj = self.pool['sale.order']
111 sale = sale_obj.browse(cr, uid, ref("sale_order_partial_invoice.sale_order1"), context=context)
112 wiz_data = {'name': 'wiz1',
113 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 1}),
114 (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 1}),
115 (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 4}),
116 ],
117 }
118 id = self.create(cr, uid, wiz_data, context=context)
119 self.create_invoice(cr, uid, [id], context=context)
120-
121 Check the 1st invoice is created
122-
123 !python {model: sale.order}: |
124 order = self.browse(cr, uid, ref('sale_order1'))
125 assert len(order.invoice_ids) == 1, "I should have 1 invoice"
126-
127 Check qty_invoiced 4
128-
129 !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
130 - order_line[0].qty_invoiced == 1
131 - order_line[1].qty_invoiced == 1
132 - order_line[2].qty_invoiced == 4
133-
134 Check qty_delivered 4
135-
136 !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
137 - order_line[0].qty_delivered == 1
138 - order_line[1].qty_delivered == 1
139 - order_line[2].qty_delivered == 4
140-
141 Check SO state
142-
143 !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
144 - state == 'manual'
145-
146 Partial delivery 2
147-
148 !python {model: stock.picking}: |
149 delivery_orders = self.search(cr, uid, [('sale_id','=',ref("sale_order_partial_invoice.sale_order1")), ('state', '!=', 'done')])
150 picking = self.browse(cr, uid, delivery_orders[-1], context=context)
151 if picking.force_assign(cr, uid, picking):
152 assert len(picking.move_lines) == 2, "wrong count of stock moves"
153 partial_moves = {}
154 for move in picking.move_lines:
155 partial_moves['move%s' % move.id] = {'product_qty': move.product_qty, 'product_uom': ref('product.product_uom_unit')}
156 picking.do_partial(partial_moves, context=context)
157-
158 Create partial invoice 2
159-
160 !python {model: sale.order.line.invoice.partially}: |
161 sale_obj = self.pool['sale.order']
162 sale = sale_obj.browse(cr, uid, ref("sale_order1"), context=context)
163 wiz_data = {'name': 'wiz2',
164 'line_ids': [(0, 0, {'sale_order_line_id': sale.order_line[0].id, 'quantity': 2}),
165 (0, 0, {'sale_order_line_id': sale.order_line[1].id, 'quantity': 2}),
166 (0, 0, {'sale_order_line_id': sale.order_line[2].id, 'quantity': 8}),
167 ],
168 }
169 id = self.create(cr, uid, wiz_data, context=context)
170 self.create_invoice(cr, uid, [id], context=context)
171-
172 Check the 2nd invoice is created
173-
174 !python {model: sale.order}: |
175 order = self.browse(cr, uid, ref('sale_order1'))
176 assert len(order.invoice_ids) == 2, "I should have 2 invoices"
177-
178 Check qty_invoiced 5
179-
180 !assert {model: sale.order, id: sale_order1, string: qty_invoiced not correctly implemented}:
181 - order_line[0].qty_invoiced == 3
182 - order_line[1].qty_invoiced == 3
183 - order_line[2].qty_invoiced == 12
184-
185 Check qty_delivered 5
186-
187 !assert {model: sale.order, id: sale_order1, string: qty_delivered not correctly implemented}:
188 - order_line[0].qty_delivered == 3
189 - order_line[1].qty_delivered == 3
190 - order_line[2].qty_delivered == 12
191-
192 Check SO state
193-
194 !assert {model: sale.order, id: sale_order1, string: wrong SO State}:
195 - state == 'manual'
196-
197 I confirm the invoices
198-
199 !python {model: sale.order}: |
200 import netsvc
201 wf_service = netsvc.LocalService("workflow")
202 so = self.browse(cr, uid, ref("sale_order1"))
203 for invoice in so.invoice_ids:
204 wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
205-
206 I pay the invoices.
207-
208 !python {model: account.invoice}: |
209 sale_order = self.pool.get('sale.order')
210 order = sale_order.browse(cr, uid, ref("sale_order1"))
211 journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
212 for invoice in order.invoice_ids:
213 invoice.pay_and_reconcile(
214 invoice.amount_total, ref('account.cash'), ref('account.period_8'),
215 journal_ids[0], ref('account.cash'),
216 ref('account.period_8'), journal_ids[0],
217 name='test')
218-
219 I run the scheduler (required to get the correct sale order state)
220-
221 !python {model: procurement.order}: |
222 self.run_scheduler(cr, uid)
223-
224 I check sale order
225-
226 !python {model: sale.order}: |
227 sale_order = self.browse(cr, uid, ref("sale_order1"))
228 assert sale_order.invoice_ids, "Invoice should be created."
229 assert sale_order.invoice_exists, "Order is not invoiced."
230 assert sale_order.invoiced, "Order is not paid."
231 assert sale_order.state == 'done', 'Order should be Done.'

Subscribers

People subscribed via source and target branches