Merge lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:~humanitarian-core-editors/openerp-humanitarian-ngo/ngo-addons

Proposed by Nicolas Bessi - Camptocamp
Status: Merged
Approved by: Yannick Vaucher @ Camptocamp
Approved revision: 137
Merged at revision: 109
Proposed branch: lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi
Merge into: lp:~humanitarian-core-editors/openerp-humanitarian-ngo/ngo-addons
Diff against target: 2358 lines (+2126/-8)
28 files modified
framework_agreement_requisition/__init__.py (+21/-0)
framework_agreement_requisition/__openerp__.py (+59/-0)
framework_agreement_requisition/model/__init__.py (+22/-0)
framework_agreement_requisition/model/purchase.py (+106/-0)
framework_agreement_requisition/model/purchase_requisition.py (+93/-0)
framework_agreement_requisition/purchase_workflow.xml (+17/-0)
framework_agreement_requisition/requisition_workflow.xml (+18/-0)
framework_agreement_requisition/test/agreement_requisition.yml (+98/-0)
framework_agreement_requisition/view/purchase_requisition_view.xml (+39/-0)
framework_agreement_sourcing/__init__.py (+21/-0)
framework_agreement_sourcing/__openerp__.py (+55/-0)
framework_agreement_sourcing/model/__init__.py (+26/-0)
framework_agreement_sourcing/model/adapter_util.py (+116/-0)
framework_agreement_sourcing/model/logistic_requisition.py (+243/-0)
framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py (+136/-0)
framework_agreement_sourcing/model/logistic_requisition_source.py (+369/-0)
framework_agreement_sourcing/model/purchase.py (+91/-0)
framework_agreement_sourcing/model/sale_order.py (+59/-0)
framework_agreement_sourcing/tests/__init__.py (+25/-0)
framework_agreement_sourcing/tests/common.py (+143/-0)
framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py (+74/-0)
framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py (+135/-0)
framework_agreement_sourcing/view/requisition_view.xml (+119/-0)
logistic_requisition/model/logistic_requisition.py (+3/-1)
logistic_requisition/model/purchase.py (+23/-1)
logistic_requisition/model/sale_order.py (+10/-1)
logistic_requisition/view/logistic_requisition.xml (+4/-4)
logistic_requisition/wizard/cost_estimate.py (+1/-1)
To merge this branch: bzr merge lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi
Reviewer Review Type Date Requested Status
Yannick Vaucher @ Camptocamp code review Approve
Joël Grand-Guillaume @ camptocamp code review + test Approve
Romain Deheele - Camptocamp (community) code review, test Approve
Review via email: mp+196676@code.launchpad.net

Description of the change

Add sourcing using framework agreement
Fix small ergonomic topic to have a more common workflow

After functional review fixes following points:

[FIX] Coste estimate can not be duplicate if generated by LR
[FIX] LR origin not put on Coste estimate origin field
[FIX] propagate framework agreement on purchase order
[FIX] propagation of incoterm on tender form LR
[IMP] logistic requisition create draft PO and not draf bid if LTA

To post a comment you must log in.
Revision history for this message
Romain Deheele - Camptocamp (romaindeheele) wrote :

Hello,

Just 2 minor points:
- an empty file "touch" commited
- last test's docstring in "framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py" is started, not finished...

Our discussion has clarified all other points.
(as another addon planned to hide framework_agreement_tender checkbox when we come from a logistic requisition)

Romain

review: Approve (code review, test)
118. By Nicolas Bessi - Camptocamp

[FIX] remove faulty button on requisition line view. You can not directly create coste estimate in list mode

119. By Nicolas Bessi - Camptocamp

[FIX] rm dead commented code

120. By Nicolas Bessi - Camptocamp

[IMP] logistic requisition create draft PO and not draf bid if LTA

121. By Nicolas Bessi - Camptocamp

[FIX] propagation of incoterm on tender form LR

122. By Nicolas Bessi - Camptocamp

[FIX] propagate framework agreement on purchase order

123. By Nicolas Bessi - Camptocamp

[FIX] LR origin not put on Coste estimate origin field

124. By Nicolas Bessi - Camptocamp

[FIX] Coste estimate can not be duplicate if generated by LR

125. By Nicolas Bessi - Camptocamp

[MRG] from head

126. By Nicolas Bessi - Camptocamp

[FIX] only agreement PO must be in state confirmed when Cost Estimate is confirmed

Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi,

Thanks for the MP ! A few remarks on my side:

 * Please add a clearer description in the module framework_agreement_requisition __openerp__.py that explain the business need/cases that it cover. Be reading the current one, I didn't get his purpose. It'll be also appreciable to describe the standard flow that this module suggest : starts from a po to land in a FA with all steps described.

 * The same for the module : framework_agreement_sourcing. A better explanation is needed. Describe how a FA sourcing workflow is defined/used in a standard case to help the user understand.

I know it takes time, but for newcomers it'll be a real time saver to have those explanation. Even, I would ask Romain to redact it ! So you con confirm it's correct by having a second eye on it and ensuring the description will be understandable by another person.

A part from that, seems a great work, thank you !

Regards,

Joël

review: Needs Fixing (code review, no tests)
127. By Nicolas Bessi - Camptocamp

[FIX] constraint to support service in lta flow

128. By Nicolas Bessi - Camptocamp

[IMP] Manifest description

129. By Nicolas Bessi - Camptocamp

[IMP] Manifest description

130. By Nicolas Bessi - Camptocamp

[TYPO]

131. By Nicolas Bessi - Camptocamp

[FIX] invoice name should not be overriden

132. By Nicolas Bessi - Camptocamp

[FIX] constraint that were set on req line instead of po line

133. By Nicolas Bessi - Camptocamp

[FIX] quantity_bid constraint

134. By Nicolas Bessi - Camptocamp

[FIX] LTA po support dropshipping

135. By Nicolas Bessi - Camptocamp

[FIX] binding of PO with SO this parts has probalbly to be refactored because we do not anticipate the complexity of underling layers

136. By Joël Grand-Guillaume @ camptocamp

[FIX] Fix the problem of the procurement stick in state 'running' once product are delivered from PO. Now proc of product of type service are handled correctly

137. By Romain Deheele - Camptocamp

[MRG] add domain on LR to filter on partner of type customer for customer field

Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi,

This LGTM, as all the correction have been made here :

https://code.launchpad.net/~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_other_procurement_method-nbi/+merge/205123

So I suggest merging this one first in the trunk and then proposed the *-add-other_procurement_method_nbi to be merged in trunk.

Regards

review: Approve (code review + test)
Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

LGTM

review: Approve (code review)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'framework_agreement_requisition'
2=== added file 'framework_agreement_requisition/__init__.py'
3--- framework_agreement_requisition/__init__.py 1970-01-01 00:00:00 +0000
4+++ framework_agreement_requisition/__init__.py 2014-02-06 10:43:21 +0000
5@@ -0,0 +1,21 @@
6+# -*- coding: utf-8 -*-
7+##############################################################################
8+#
9+# Author: Nicolas Bessi
10+# Copyright 2013 Camptocamp SA
11+#
12+# This program is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Affero General Public License as
14+# published by the Free Software Foundation, either version 3 of the
15+# License, or (at your option) any later version.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU Affero General Public License for more details.
21+#
22+# You should have received a copy of the GNU Affero General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+#
25+##############################################################################
26+from . import model
27
28=== added file 'framework_agreement_requisition/__openerp__.py'
29--- framework_agreement_requisition/__openerp__.py 1970-01-01 00:00:00 +0000
30+++ framework_agreement_requisition/__openerp__.py 2014-02-06 10:43:21 +0000
31@@ -0,0 +1,59 @@
32+# -*- coding: utf-8 -*-
33+##############################################################################
34+#
35+# Author: Nicolas Bessi
36+# Copyright 2013 Camptocamp SA
37+#
38+# This program is free software: you can redistribute it and/or modify
39+# it under the terms of the GNU Affero General Public License as
40+# published by the Free Software Foundation, either version 3 of the
41+# License, or (at your option) any later version.
42+#
43+# This program is distributed in the hope that it will be useful,
44+# but WITHOUT ANY WARRANTY; without even the implied warranty of
45+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46+# GNU Affero General Public License for more details.
47+#
48+# You should have received a copy of the GNU Affero General Public License
49+# along with this program. If not, see <http://www.gnu.org/licenses/>.
50+#
51+##############################################################################
52+{'name': 'Framework Agreement Negociation',
53+ 'version': '0.1',
54+ 'author': 'Camptocamp',
55+ 'maintainer': 'Camptocamp',
56+ 'category': 'NGO',
57+ 'complexity': 'normal',
58+ 'depends': ['purchase_requisition',
59+ 'purchase_requisition_extended',
60+ 'framework_agreement'],
61+ 'description': """
62+Negociate framework agreement using tender process
63+==================================================
64+
65+This will allows you to use "The calls for Bids" model
66+ to negociate agreement.
67+
68+To to so you have too check the box "Negociate Agreement".
69+
70+The module add a state "Agreement selected" on tender and PO.
71+
72+
73+These will be the final state once you have choosen
74+the agreement that fit your needs the best.
75+
76+Once the selection is done juste use the button "Agreement selected" on tender
77+That will close flow of tender and related PO accordingly.
78+
79+""",
80+ 'website': 'http://www.camptocamp.com',
81+ 'data': ['requisition_workflow.xml',
82+ 'purchase_workflow.xml',
83+ 'view/purchase_requisition_view.xml'],
84+ 'demo': [],
85+ 'test': ['test/agreement_requisition.yml'],
86+ 'installable': True,
87+ 'auto_install': False,
88+ 'license': 'AGPL-3',
89+ 'application': False,
90+ }
91
92=== added directory 'framework_agreement_requisition/model'
93=== added file 'framework_agreement_requisition/model/__init__.py'
94--- framework_agreement_requisition/model/__init__.py 1970-01-01 00:00:00 +0000
95+++ framework_agreement_requisition/model/__init__.py 2014-02-06 10:43:21 +0000
96@@ -0,0 +1,22 @@
97+# -*- coding: utf-8 -*-
98+##############################################################################
99+#
100+# Author: Nicolas Bessi
101+# Copyright 2013 Camptocamp SA
102+#
103+# This program is free software: you can redistribute it and/or modify
104+# it under the terms of the GNU Affero General Public License as
105+# published by the Free Software Foundation, either version 3 of the
106+# License, or (at your option) any later version.
107+#
108+# This program is distributed in the hope that it will be useful,
109+# but WITHOUT ANY WARRANTY; without even the implied warranty of
110+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
111+# GNU Affero General Public License for more details.
112+#
113+# You should have received a copy of the GNU Affero General Public License
114+# along with this program. If not, see <http://www.gnu.org/licenses/>.
115+#
116+##############################################################################
117+from . import purchase_requisition
118+from . import purchase
119
120=== added file 'framework_agreement_requisition/model/purchase.py'
121--- framework_agreement_requisition/model/purchase.py 1970-01-01 00:00:00 +0000
122+++ framework_agreement_requisition/model/purchase.py 2014-02-06 10:43:21 +0000
123@@ -0,0 +1,106 @@
124+# -*- coding: utf-8 -*-
125+##############################################################################
126+#
127+# Author: Nicolas Bessi
128+# Copyright 2013 Camptocamp SA
129+#
130+# This program is free software: you can redistribute it and/or modify
131+# it under the terms of the GNU Affero General Public License as
132+# published by the Free Software Foundation, either version 3 of the
133+# License, or (at your option) any later version.
134+#
135+# This program is distributed in the hope that it will be useful,
136+# but WITHOUT ANY WARRANTY; without even the implied warranty of
137+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138+# GNU Affero General Public License for more details.
139+#
140+# You should have received a copy of the GNU Affero General Public License
141+# along with this program. If not, see <http://www.gnu.org/licenses/>.
142+#
143+##############################################################################
144+from openerp import netsvc
145+from openerp.osv import orm
146+
147+SELECTED_STATE = ('agreement_selected', 'Agreement selected')
148+AGR_SELECT = 'agreement_selected'
149+
150+
151+class purchase_order(orm.Model):
152+ """Add workflow behavior"""
153+
154+ _inherit = "purchase.order"
155+
156+ def __init__(self, pool, cr):
157+ """Add a new state value using PO class property"""
158+ if SELECTED_STATE not in super(purchase_order, self).STATE_SELECTION:
159+ super(purchase_order, self).STATE_SELECTION.append(SELECTED_STATE)
160+ return super(purchase_order, self).__init__(pool, cr)
161+
162+ def select_agreement(self, cr, uid, agr_id, context=None):
163+ """Pass PO in state 'Agreement selected'"""
164+ if isinstance(agr_id, (list, tuple)):
165+ assert len(agr_id) == 1
166+ agr_id = agr_id[0]
167+ wf_service = netsvc.LocalService("workflow")
168+ return wf_service.trg_validate(uid, 'purchase.order',
169+ agr_id, 'select_agreement', cr)
170+
171+ def po_tender_agreement_selected(self, cr, uid, ids, context=None):
172+ """Workflow function that write state 'Agreement selected'"""
173+ return self.write(cr, uid, ids, {'state': AGR_SELECT},
174+ context=context)
175+
176+
177+class purchase_order_line(orm.Model):
178+ """Add make_agreement function"""
179+
180+ _inherit = "purchase.order.line"
181+
182+ # Did you know a good way to supress SQL constraint to add
183+ # Python constraint...
184+ _sql_constraints = [
185+ ('quantity_bid', 'CHECK(true)',
186+ 'Selected quantity must be less or equal than the quantity in the bid'),
187+ ]
188+
189+ def _check_quantity_bid(self, cr, uid, ids, context=None):
190+ for line in self.browse(cr, uid, ids, context=context):
191+ if line.order_id.framework_agreement_id:
192+ continue
193+ if line.product_id.type == 'product' and not line.quantity_bid <= line.product_qty:
194+ return False
195+ return True
196+
197+ _constraints = [
198+ (_check_quantity_bid,
199+ 'Selected quantity must be less or equal than the quantity in the bid',
200+ [])
201+ ]
202+ def _agreement_data(self, cr, uid, po_line, origin, context=None):
203+ """Get agreement values from PO line
204+
205+ :param po_line: Po line records
206+
207+ :returns: agreement dict to be used by orm.Model.create
208+ """
209+ vals = {}
210+ vals['supplier_id'] = po_line.order_id.partner_id.id
211+ vals['product_id'] = po_line.product_id.id
212+ vals['quantity'] = po_line.product_qty
213+ vals['origin'] = origin if origin else False
214+ return vals
215+
216+ def make_agreement(self, cr, uid, line_id, origin, context=None):
217+ """ generate a draft framework agreement
218+
219+ :returns: a record of LTA
220+
221+ """
222+ agr_model = self.pool['framework.agreement']
223+ if isinstance(line_id, (list, tuple)):
224+ assert len(line_id) == 1
225+ line_id = line_id[0]
226+ current = self.browse(cr, uid, line_id, context=context)
227+ vals = self._agreement_data(cr, uid, current, origin, context=context)
228+ agr_id = agr_model.create(cr, uid, vals, context=context)
229+ return agr_model.browse(cr, uid, agr_id, context=context)
230
231=== added file 'framework_agreement_requisition/model/purchase_requisition.py'
232--- framework_agreement_requisition/model/purchase_requisition.py 1970-01-01 00:00:00 +0000
233+++ framework_agreement_requisition/model/purchase_requisition.py 2014-02-06 10:43:21 +0000
234@@ -0,0 +1,93 @@
235+# -*- coding: utf-8 -*-
236+##############################################################################
237+#
238+# Author: Nicolas Bessi
239+# Copyright 2013 Camptocamp SA
240+#
241+# This program is free software: you can redistribute it and/or modify
242+# it under the terms of the GNU Affero General Public License as
243+# published by the Free Software Foundation, either version 3 of the
244+# License, or (at your option) any later version.
245+#
246+# This program is distributed in the hope that it will be useful,
247+# but WITHOUT ANY WARRANTY; without even the implied warranty of
248+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
249+# GNU Affero General Public License for more details.
250+#
251+# You should have received a copy of the GNU Affero General Public License
252+# along with this program. If not, see <http://www.gnu.org/licenses/>.
253+#
254+##############################################################################
255+from itertools import chain
256+from openerp import netsvc
257+from openerp.osv import orm, fields
258+from openerp.tools.translate import _
259+from .purchase import AGR_SELECT as PO_AGR_SELECT
260+
261+SELECTED_STATE = ('agreement_selected', 'Agreement selected')
262+AGR_SELECT = 'agreement_selected'
263+
264+
265+class purchase_requisition(orm.Model):
266+ """Add support to negociate LTA using tender process"""
267+
268+ def __init__(self, pool, cr):
269+ """Nasty hack to add fields to select fields
270+
271+ We do this in order not to compromising other state added
272+ by other addons that are not in inheritance chain...
273+
274+ """
275+ sel = super(purchase_requisition, self)._columns['state']
276+ if SELECTED_STATE not in sel.selection:
277+ sel.selection.append(SELECTED_STATE)
278+ return super(purchase_requisition, self).__init__(pool, cr)
279+
280+ _inherit = "purchase.requisition"
281+ _columns = {
282+ 'framework_agreement_tender': fields.boolean('Negociate Agreement'),
283+ }
284+
285+ def tender_agreement_selected(self, cr, uid, ids, context=None):
286+ """Workflow function that write state 'Agreement selected'"""
287+ return self.write(cr, uid, ids, {'state': AGR_SELECT},
288+ context=context)
289+
290+ def select_agreement(self, cr, uid, agr_id, context=None):
291+ """Pass tender to state 'Agreement selected'"""
292+ if isinstance(agr_id, (list, tuple)):
293+ assert len(agr_id) == 1
294+ agr_id = agr_id[0]
295+ wf_service = netsvc.LocalService("workflow")
296+ return wf_service.trg_validate(uid, 'purchase.requisition',
297+ agr_id, 'select_agreement', cr)
298+
299+ def agreement_selected(self, cr, uid, ids, context=None):
300+ """Tells tender that an agreement has been selected"""
301+ if isinstance(ids, (int, long)):
302+ ids = [ids]
303+ for req in self.browse(cr, uid, ids, context=context):
304+ if not req.framework_agreement_tender:
305+ raise orm.except_orm(_('Invalid tender'),
306+ _('Request is not of type agreement'))
307+ self.select_agreement(cr, uid, req.id, context=context)
308+ req.refresh()
309+ if req.state != AGR_SELECT:
310+ raise RuntimeError('requisiton %s does not pass to state'
311+ ' agreement_selected' %
312+ req.name)
313+ rfqs = chain.from_iterable(req_line.purchase_line_ids
314+ for req_line in req.line_ids)
315+ rfqs = [rfq for rfq in rfqs if rfq.state == 'confirmed']
316+ if not rfqs:
317+ raise orm.except_orm(_('No confirmed RFQ related to tender'),
318+ _('Please choose at least one'))
319+ for rfq in rfqs:
320+ rfq.make_agreement(req.name)
321+ p_order = rfq.order_id
322+ p_order.select_agreement()
323+ p_order.refresh()
324+ if p_order.state != PO_AGR_SELECT:
325+ raise RuntimeError('Purchase order %s does not pass to %' %
326+ (p_order.name, PO_AGR_SELECT))
327+ return True
328
329=== added file 'framework_agreement_requisition/purchase_workflow.xml'
330--- framework_agreement_requisition/purchase_workflow.xml 1970-01-01 00:00:00 +0000
331+++ framework_agreement_requisition/purchase_workflow.xml 2014-02-06 10:43:21 +0000
332@@ -0,0 +1,17 @@
333+<?xml version="1.0" encoding="utf-8"?>
334+<openerp>
335+ <data>
336+ <record id="act_po_agreement_selected" model="workflow.activity">
337+ <field name="wkf_id" ref="purchase.purchase_order"/>
338+ <field name="name">Agreement selected</field>
339+ <field name="kind">function</field>
340+ <field name="action">po_tender_agreement_selected()</field>
341+ <field name="flow_stop">True</field>
342+ </record>
343+ <record id="trans_po_agreement_selected" model="workflow.transition">
344+ <field name="act_from" ref="purchase.act_bid"/>
345+ <field name="act_to" ref="act_po_agreement_selected"/>
346+ <field name="signal">select_agreement</field>
347+ </record>
348+ </data>
349+</openerp>
350
351=== added file 'framework_agreement_requisition/requisition_workflow.xml'
352--- framework_agreement_requisition/requisition_workflow.xml 1970-01-01 00:00:00 +0000
353+++ framework_agreement_requisition/requisition_workflow.xml 2014-02-06 10:43:21 +0000
354@@ -0,0 +1,18 @@
355+<?xml version="1.0" encoding="utf-8"?>
356+<openerp>
357+ <data>
358+ <record id="act_agreement_selected" model="workflow.activity">
359+ <field name="wkf_id" ref="purchase_requisition.purchase_requisition_workflow"/>
360+ <field name="name">Agreement selected</field>
361+ <field name="kind">function</field>
362+ <field name="action">tender_agreement_selected()</field>
363+ <field name="flow_stop">True</field>
364+ </record>
365+
366+ <record id="trans_agreement_selected" model="workflow.transition">
367+ <field name="act_from" ref="purchase_requisition_extended.act_closed"/>
368+ <field name="act_to" ref="act_agreement_selected"/>
369+ <field name="signal">select_agreement</field>
370+ </record>
371+ </data>
372+</openerp>
373
374=== added directory 'framework_agreement_requisition/test'
375=== added file 'framework_agreement_requisition/test/agreement_requisition.yml'
376--- framework_agreement_requisition/test/agreement_requisition.yml 1970-01-01 00:00:00 +0000
377+++ framework_agreement_requisition/test/agreement_requisition.yml 2014-02-06 10:43:21 +0000
378@@ -0,0 +1,98 @@
379+-
380+ Standard flow of a Call for agreement Bids in mode open
381+-
382+ Create Call for Bids
383+-
384+ !record {model: purchase.requisition, id: purchase_requisition_agreement}:
385+ date_start: '2013-08-02 00:00:00'
386+ date_end: '2013-08-30 00:00:00'
387+ bid_tendering_mode: 'open'
388+ schedule_date: '2013-09-30'
389+ req_validity: '2013-09-10'
390+ framework_agreement_tender: True
391+ line_ids:
392+ - product_id: product.product_product_15
393+ product_qty: 2500.0
394+-
395+ Confirm Call
396+-
397+ !python {model: purchase.requisition}: |
398+ import netsvc
399+ wf_service = netsvc.LocalService("workflow")
400+ wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'sent_suppliers', cr)
401+-
402+ Create RFQ1. I run the 'Request a quotation' wizard. I fill the supplier.
403+-
404+ !record {model: purchase.requisition.partner, id: purchase_requisition_agreement_partner1_create}:
405+ partner_id: base.res_partner_2
406+-
407+ Create RFQ1. I confirm the wizard.
408+-
409+ !python {model: purchase.requisition.partner}: |
410+ self.create_order(cr, uid, [ref("purchase_requisition_agreement_partner1_create")],{
411+ 'active_model': 'purchase.requisition',
412+ 'active_id': ref("purchase_requisition_agreement"),
413+ 'active_ids': [ref("purchase_requisition_agreement")],
414+ })
415+-
416+ I encode the bid. I set a 300 price on the line.
417+-
418+ !python {model: purchase.requisition}: |
419+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
420+ assert len(purchase_req.purchase_ids) == 1, "There must be 1 RFQs linked to this Call for bids"
421+ price = 300
422+ for rfq in purchase_req.purchase_ids:
423+ for line in rfq.order_line:
424+ self.pool.get('purchase.order.line').write(cr, uid, [line.id], {'price_unit': price})
425+
426+-
427+ I send the RFQ. For this, I print the RFQ.
428+-
429+ !python {model: purchase.requisition}: |
430+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
431+ for rfq in purchase_req.purchase_ids:
432+ self.pool.get('purchase.order').print_quotation(cr, uid, [rfq.id])
433+-
434+ I run the 'Bid encoded' wizard of bid1. I fill the date.
435+-
436+ !record {model: purchase.action_modal_datetime, id: purchase_requisition_agreement_bid1_bidencoded}:
437+ datetime: '2013-08-13 00:00:00'
438+-
439+ I launch wizard action.
440+-
441+ !python {model: purchase.action_modal_datetime}: |
442+ purchase_req = self.pool['purchase.requisition'].browse(cr, uid, ref("purchase_requisition_agreement"))
443+ po_id = purchase_req.purchase_ids[0].id
444+ self.action(cr, uid, [ref('purchase_requisition_agreement_bid1_bidencoded')],
445+ {'action': 'bid_received_ok',
446+ 'active_id': po_id,
447+ 'active_ids': [po_id],
448+ 'active_model': 'purchase.order',
449+ 'default_datetime': '2013-08-13 00:00:00',
450+ 'uid': 1})
451+-
452+ I close the Call for bids and move to bids selection
453+-
454+ !python {model: purchase.requisition}: |
455+ import netsvc
456+ wf_service = netsvc.LocalService("workflow")
457+ wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'open_bid', cr)
458+
459+-
460+ In the bids selection, I confirm line 1 of bid 1
461+-
462+ !python {model: purchase.requisition}: |
463+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
464+ self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[0].order_line[0].id])
465+-
466+ I close the call for bids
467+-
468+ !python {model: purchase.requisition}: |
469+ self.close_callforbids(cr, uid, [ref("purchase_requisition_agreement")])
470+-
471+ I mark the tender as agreement selected
472+-
473+ !python {model: purchase.requisition}: |
474+ self.agreement_selected(cr, uid, ref("purchase_requisition_agreement"))
475+ purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
476+ assert purchase_req.state == 'agreement_selected'
477
478=== added directory 'framework_agreement_requisition/view'
479=== added file 'framework_agreement_requisition/view/purchase_requisition_view.xml'
480--- framework_agreement_requisition/view/purchase_requisition_view.xml 1970-01-01 00:00:00 +0000
481+++ framework_agreement_requisition/view/purchase_requisition_view.xml 2014-02-06 10:43:21 +0000
482@@ -0,0 +1,39 @@
483+<?xml version="1.0" encoding="utf-8"?>
484+<openerp>
485+ <data noupdate="0">
486+ <record model="ir.ui.view" id="view_purchase_requisition_form_agreement">
487+ <field name="name">purchase.requisition.form.inherit.aggrement.button</field>
488+ <field name="model">purchase.requisition</field>
489+ <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_form"/>
490+ <field name="arch" type="xml">
491+ <field name="multiple_rfq_per_supplier"
492+ position="after">
493+ <field name="framework_agreement_tender"/>
494+ </field>
495+ <button name="cancel_requisition" position="after">
496+ <button name="agreement_selected"
497+ type="object"
498+ class="po_buttons oe_form_buttons"
499+ attrs="{'invisible': ['|', ('framework_agreement_tender', '=', False), ('state', '!=', 'closed')]}"
500+ string="Framework agreement selected"/>
501+ </button>
502+ </field>
503+ </record>
504+
505+ <record model="ir.ui.view" id="view_purchase_requisition_filter">
506+ <field name="name">purchase.requisition.form.inherit.agreement.filter</field>
507+ <field name="model">purchase.requisition</field>
508+ <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_filter"/>
509+ <field name="arch" type="xml">
510+ <filter name="draft"
511+ position="after">
512+ <filter icon="terp-document-new"
513+ name="framework_agreement"
514+ string="Framework Agreement?"
515+ domain="[(framework_agreement_tender,'=',True)]"/>
516+ </filter>
517+ </field>
518+ </record>
519+
520+ </data>
521+</openerp>
522
523=== added directory 'framework_agreement_sourcing'
524=== added file 'framework_agreement_sourcing/__init__.py'
525--- framework_agreement_sourcing/__init__.py 1970-01-01 00:00:00 +0000
526+++ framework_agreement_sourcing/__init__.py 2014-02-06 10:43:21 +0000
527@@ -0,0 +1,21 @@
528+# -*- coding: utf-8 -*-
529+##############################################################################
530+#
531+# Author: Nicolas Bessi
532+# Copyright 2013 Camptocamp SA
533+#
534+# This program is free software: you can redistribute it and/or modify
535+# it under the terms of the GNU Affero General Public License as
536+# published by the Free Software Foundation, either version 3 of the
537+# License, or (at your option) any later version.
538+#
539+# This program is distributed in the hope that it will be useful,
540+# but WITHOUT ANY WARRANTY; without even the implied warranty of
541+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
542+# GNU Affero General Public License for more details.
543+#
544+# You should have received a copy of the GNU Affero General Public License
545+# along with this program. If not, see <http://www.gnu.org/licenses/>.
546+#
547+##############################################################################
548+from . import model
549
550=== added file 'framework_agreement_sourcing/__openerp__.py'
551--- framework_agreement_sourcing/__openerp__.py 1970-01-01 00:00:00 +0000
552+++ framework_agreement_sourcing/__openerp__.py 2014-02-06 10:43:21 +0000
553@@ -0,0 +1,55 @@
554+# -*- coding: utf-8 -*-
555+##############################################################################
556+#
557+# Author: Nicolas Bessi
558+# Copyright 2013 Camptocamp SA
559+#
560+# This program is free software: you can redistribute it and/or modify
561+# it under the terms of the GNU Affero General Public License as
562+# published by the Free Software Foundation, either version 3 of the
563+# License, or (at your option) any later version.
564+#
565+# This program is distributed in the hope that it will be useful,
566+# but WITHOUT ANY WARRANTY; without even the implied warranty of
567+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
568+# GNU Affero General Public License for more details.
569+#
570+# You should have received a copy of the GNU Affero General Public License
571+# along with this program. If not, see <http://www.gnu.org/licenses/>.
572+#
573+##############################################################################
574+{'name': 'Framework agreement integration in sourcing',
575+ 'version': '0.1',
576+ 'author': 'Camptocamp',
577+ 'maintainer': 'Camptocamp',
578+ 'category': 'NGO',
579+ 'complexity': 'normal',
580+ 'depends': ['framework_agreement', 'logistic_requisition'],
581+ 'description': """
582+Automatically source logistic order from framework agreement
583+============================================================
584+
585+If you have a framework agreement negociated for the current product in
586+your logistic requisition. If the date and state of agreement are OK,
587+agreement will be used as source for the concerned source lines
588+of your request.
589+
590+In this case tender flow is byassed and confirmed PO will be generated
591+when logistic requisition is confirmed.
592+
593+By default the sourcing process will look in all agreements for a product
594+and use them one after the other as long as possible sorted by price.
595+
596+You can prevent this behavior by forcing only one agreement per product at
597+the same time in company.
598+
599+""",
600+ 'website': 'http://www.camptocamp.com',
601+ 'data': ['view/requisition_view.xml'],
602+ 'demo': [],
603+ 'test': [],
604+ 'installable': True,
605+ 'auto_install': False,
606+ 'license': 'AGPL-3',
607+ 'application': False,
608+ }
609
610=== added directory 'framework_agreement_sourcing/i18n'
611=== added directory 'framework_agreement_sourcing/model'
612=== added file 'framework_agreement_sourcing/model/__init__.py'
613--- framework_agreement_sourcing/model/__init__.py 1970-01-01 00:00:00 +0000
614+++ framework_agreement_sourcing/model/__init__.py 2014-02-06 10:43:21 +0000
615@@ -0,0 +1,26 @@
616+# -*- coding: utf-8 -*-
617+##############################################################################
618+#
619+# Author: Nicolas Bessi
620+# Copyright 2013 Camptocamp SA
621+#
622+# This program is free software: you can redistribute it and/or modify
623+# it under the terms of the GNU Affero General Public License as
624+# published by the Free Software Foundation, either version 3 of the
625+# License, or (at your option) any later version.
626+#
627+# This program is distributed in the hope that it will be useful,
628+# but WITHOUT ANY WARRANTY; without even the implied warranty of
629+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
630+# GNU Affero General Public License for more details.
631+#
632+# You should have received a copy of the GNU Affero General Public License
633+# along with this program. If not, see <http://www.gnu.org/licenses/>.
634+#
635+##############################################################################
636+from . import logistic_requisition
637+from . import logistic_requisition_source
638+from . import purchase
639+from . import adapter_util
640+from . import sale_order
641+from . import logistic_requisition_cost_estimate
642
643=== added file 'framework_agreement_sourcing/model/adapter_util.py'
644--- framework_agreement_sourcing/model/adapter_util.py 1970-01-01 00:00:00 +0000
645+++ framework_agreement_sourcing/model/adapter_util.py 2014-02-06 10:43:21 +0000
646@@ -0,0 +1,116 @@
647+# -*- coding: utf-8 -*-
648+##############################################################################
649+#
650+# Author: Nicolas Bessi
651+# Copyright 2013 Camptocamp SA
652+#
653+# This program is free software: you can redistribute it and/or modify
654+# it under the terms of the GNU Affero General Public License as
655+# published by the Free Software Foundation, either version 3 of the
656+# License, or (at your option) any later version.
657+#
658+# This program is distributed in the hope that it will be useful,
659+# but WITHOUT ANY WARRANTY; without even the implied warranty of
660+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
661+# GNU Affero General Public License for more details.
662+#
663+# You should have received a copy of the GNU Affero General Public License
664+# along with this program. If not, see <http://www.gnu.org/licenses/>.
665+#
666+##############################################################################
667+"""Provides basic mechanism to unify the way records are transformed into
668+other records
669+
670+"""
671+
672+from openerp.osv import orm
673+
674+
675+class BrowseAdapterSourceMixin(object):
676+ """Mixin class used by Model that are transformation sources"""
677+
678+ def _company(self, cr, uid, context):
679+ """Return company id
680+
681+ :returns: company id
682+
683+ """
684+ return self.pool['res.company']._company_default_get(cr, uid, 'purchase.order',
685+ context=context)
686+
687+ def _direct_map(self, line, mapping, context=None):
688+ """Take a dict of left key right key and make direct mapping
689+ into the model
690+
691+ :returns: data dict ready to be used
692+ """
693+ data = {}
694+ for po_key, source_key in mapping.iteritems():
695+ value = line[source_key]
696+ if isinstance(value, orm.browse_record):
697+ value = value.id
698+ elif isinstance(value, orm.browse_null):
699+ value = False
700+ elif isinstance(value, orm.browse_record_list):
701+ raise NotImplementedError('List are not supported in direct map')
702+
703+ data[po_key] = value
704+ return data
705+
706+
707+class BrowseAdapterMixin(object):
708+
709+ def _do_checks(self, cr, uid, model, data, context=None):
710+ """Perform validation check of adapted data.
711+
712+ All missing or incorrect values are return at once.
713+
714+ :returns: array of exceptions
715+
716+ """
717+ required_keys = set(k for k, v in model._columns.iteritems()
718+ if v.required and not getattr(v, '_fnct', False))
719+ empty_required = set(x for x in data
720+ if x in required_keys and not data[x])
721+ missing_required = required_keys - set(data.keys())
722+ missing_required.update(empty_required)
723+ if missing_required:
724+ return[ValueError('Following value are missing or False'
725+ ' while adapting %s: %s' %
726+ (model._name, ", ".join(missing_required)))]
727+ return []
728+
729+ def _validate_adapted_data(self, cr, uid, model, data, context=None):
730+ """Perform validation check of adapted data.
731+
732+ All missing or incorrect values are return at once.
733+
734+ :returns: validated data or raise Value error
735+
736+ """
737+ errors = self._do_checks(cr, uid, model, data, context=context)
738+ if errors:
739+ raise ValueError('Data are invalid for following reason %s' %
740+ ("\n".join(repr(e) for e in errors)))
741+ return data
742+
743+ def _adapt_origin(self, cr, uid, model, origin,
744+ map_fun, post_fun=None, context=None, **kwargs):
745+ """Do transformation of source data to dest data using transforms function.
746+
747+ :param origin: source record
748+ :param map_fun: transform function
749+ :param post_fun: post transformation hook function
750+
751+ :returns: transformed data
752+
753+ """
754+ if not callable(map_fun):
755+ raise ValueError('Mapping function is not callable')
756+ if post_fun and not callable(post_fun):
757+ raise ValueError('Post hook function is not callable')
758+ data = map_fun(cr, uid, origin, context=context, **kwargs)
759+ # we complete with default
760+ missing = set(model._columns.keys()) - set(data.keys())
761+ data.update(model.default_get(cr, uid, missing, context=context))
762+ return data
763
764=== added file 'framework_agreement_sourcing/model/logistic_requisition.py'
765--- framework_agreement_sourcing/model/logistic_requisition.py 1970-01-01 00:00:00 +0000
766+++ framework_agreement_sourcing/model/logistic_requisition.py 2014-02-06 10:43:21 +0000
767@@ -0,0 +1,243 @@
768+# -*- coding: utf-8 -*-
769+##############################################################################
770+#
771+# Author: Nicolas Bessi
772+# Copyright 2013 Camptocamp SA
773+#
774+# This program is free software: you can redistribute it and/or modify
775+# it under the terms of the GNU Affero General Public License as
776+# published by the Free Software Foundation, either version 3 of the
777+# License, or (at your option) any later version.
778+#
779+# This program is distributed in the hope that it will be useful,
780+# but WITHOUT ANY WARRANTY; without even the implied warranty of
781+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
782+# GNU Affero General Public License for more details.
783+#
784+# You should have received a copy of the GNU Affero General Public License
785+# along with this program. If not, see <http://www.gnu.org/licenses/>.
786+#
787+##############################################################################
788+from collections import namedtuple
789+from openerp.tools.translate import _
790+from openerp.osv import orm
791+from .adapter_util import BrowseAdapterSourceMixin
792+from .logistic_requisition_source import AGR_PROC
793+
794+
795+class logistic_requisition_line(orm.Model, BrowseAdapterSourceMixin):
796+ """Override to enable generation of source line"""
797+
798+ _inherit = "logistic.requisition.line"
799+
800+ def _map_agr_requisiton_to_source(self, cr, uid, line, context=None,
801+ qty=0, agreement=None, **kwargs):
802+ """Prepare data dict for source line using agreement as source
803+
804+ :params line: browse record of origin requistion.line
805+ :params agreement: browse record of origin agreement
806+ :params qty: quantity to be set on source line
807+
808+ :returns: dict to be used by Model.create
809+
810+ """
811+ res = {}
812+ direct_map = {
813+ 'proposed_product_id': 'product_id',
814+ 'requisition_line_id': 'id',
815+ 'proposed_uom_id': 'requested_uom_id'}
816+
817+ if not agreement:
818+ raise ValueError("Missing agreement")
819+ if not agreement.product_id.id == line.product_id.id:
820+ raise ValueError("Product mismatch for agreement and requisition line")
821+ # currency = self._get_source_currency(cr, uid, line, context=context)
822+ res['unit_cost'] = 0.0
823+ res['proposed_qty'] = qty
824+ res['framework_agreement_id'] = agreement.id
825+ res['procurement_method'] = AGR_PROC
826+ res.update(self._direct_map(line, direct_map))
827+ return res
828+
829+ def _map_requisition_to_source(self, cr, uid, line, context=None,
830+ qty=0, **kwargs):
831+ """Prepare data dict to generate source line using requisition as source
832+
833+ :params line: browse record of origin requistion.line
834+ :params qty: quantity to be set on source line
835+
836+ :returns: dict to be used by Model.create
837+
838+ """
839+ res = {}
840+ direct_map = {'proposed_product_id': 'product_id',
841+ 'requisition_line_id': 'id',
842+ 'proposed_uom_id': 'requested_uom_id'}
843+ res['unit_cost'] = 0.0
844+ res['proposed_qty'] = qty
845+ res['framework_agreement_id'] = False
846+ res['procurement_method'] = 'procurement'
847+ res.update(self._direct_map(line, direct_map))
848+ return res
849+
850+ def _generate_lines_from_agreements(self, cr, uid, container, line,
851+ agreements, qty, currency=None, context=None):
852+ """Generate 1/n source line(s) for one requisition line.
853+
854+ This is done using available agreements.
855+ We first look for cheapeast agreement.
856+ Then if no more quantity are available and there is still remaining needs
857+ we look for next cheapest agreement or return remaining qty
858+
859+ :param container: list of agreements browse
860+ :param qty: quantity to be sourced
861+ :param line: origin requisition line
862+
863+ :returns: remaining quantity to source
864+
865+ """
866+ agreements = agreements if agreements is not None else []
867+ if currency:
868+ agreements = [x for x in agreements if x.has_currency(currency)]
869+ if not agreements:
870+ return qty
871+ agreements.sort(key=lambda x: x.get_price(qty, currency=currency))
872+ current_agr = agreements.pop(0)
873+ avail = current_agr.available_quantity
874+ if not avail:
875+ return qty
876+ avail_sold = avail - qty
877+ to_consume = qty if avail_sold >= 0 else avail
878+
879+ source_id = self.make_source_line(cr, uid, line, force_qty=to_consume,
880+ agreement=current_agr, context=context)
881+ container.append(source_id)
882+ difference = qty - to_consume
883+ if difference:
884+ return self._generate_lines_from_agreements(cr, uid, container, line,
885+ agreements, difference, context=context)
886+ else:
887+ return 0
888+
889+ def _source_lines_for_agreements(self, cr, uid, line, agreements, currency=None, context=None):
890+ """Generate 1/n source line(s) for one requisition line
891+
892+ This is done using available agreements.
893+ We first look for cheapeast agreement.
894+ Then if no more quantity are available and there is still remaining needs
895+ we look for next cheapest agreement or we create a tender source line
896+
897+ :param line: requisition line browse record
898+ :returns: (generated line ids, remaining qty not covered by agreement)
899+
900+ """
901+ Sourced = namedtuple('Sourced', ['generated', 'remaining'])
902+ qty = line.requested_qty
903+ generated = []
904+ remaining_qty = self._generate_lines_from_agreements(cr, uid, generated,
905+ line, agreements, qty,
906+ currency=currency, context=context)
907+ return Sourced(generated, remaining_qty)
908+
909+ def make_source_line(self, cr, uid, line, force_qty=None, agreement=None, context=None):
910+ """Generate a source line for a tender from a requisition line
911+
912+ :param line: browse record of origin logistic.request
913+ :param force_qty: if set this quantity will be used instead
914+ of requested quantity
915+ :returns: id of generated source line
916+
917+ """
918+ qty = force_qty if force_qty else line.requested_qty
919+ src_obj = self.pool['logistic.requisition.source']
920+ if agreement:
921+ return src_obj._make_source_line_from_origin(cr, uid, line,
922+ self._map_agr_requisiton_to_source,
923+ context=context, qty=qty,
924+ agreement=agreement)
925+ else:
926+ return src_obj._make_source_line_from_origin(cr, uid, line,
927+ self._map_requisition_to_source,
928+ context=context, qty=qty)
929+
930+ def _get_source_currency(self, cr, uid, line, context=None):
931+ agr_obj = self.pool['framework.agreement']
932+ comp_obj = self.pool['res.company']
933+ currency = line.requisition_id.get_pricelist().currency_id
934+ company_id = agr_obj._company_get(cr, uid, context=context)
935+ comp_currency = comp_obj.browse(cr, uid, company_id, context=context).currency_id
936+ if currency == comp_currency:
937+ return None
938+ return currency
939+
940+ def _generate_source_line(self, cr, uid, line, context=None):
941+ """Generate one or n source line(s) per requisition line.
942+
943+ Depending on the available resources. If there is framework agreement(s)
944+ running we generate one or n source line using agreements otherwise we generate one
945+ source line using tender process
946+
947+ :param line: browse record of origin logistic.request
948+
949+ :returns: list of generated source line ids
950+
951+ """
952+ if line.source_ids:
953+ return None
954+ agr_obj = self.pool['framework.agreement']
955+ date = line.requisition_id.date
956+ currency = self._get_source_currency(cr, uid, line, context=context)
957+ product_id = line.product_id.id
958+ agreements = agr_obj.get_all_product_agreements(cr, uid, product_id, date,
959+ context=context)
960+ generated_lines = []
961+ if agreements:
962+ line_ids, missing_qty = self._source_lines_for_agreements(cr, uid, line,
963+ agreements, currency=currency)
964+ generated_lines.extend(line_ids)
965+ if missing_qty:
966+ generated_lines.append(self.make_source_line(cr, uid, line,
967+ force_qty=missing_qty))
968+ else:
969+ generated_lines.append(self.make_source_line(cr, uid, line))
970+
971+ return generated_lines
972+
973+ def _do_confirm(self, cr, uid, ids, context=None):
974+ """Override to generate source lines from requision line.
975+
976+ Please refer to _generate_source_line documentation
977+
978+ """
979+ # TODO refactor
980+ # this should probably be in logistic_requisition module
981+ # providing a mechanism to allow each type of sourcing method
982+ # to generate source line
983+ res = super(logistic_requisition_line, self)._do_confirm(cr, uid, ids,
984+ context=context)
985+ for line_br in self.browse(cr, uid, ids, context=context):
986+ self._generate_source_line(cr, uid, line_br, context=context)
987+ return res
988+
989+
990+class logistic_requisition(orm.Model):
991+ """Add get pricelist function"""
992+
993+ _inherit = "logistic.requisition"
994+
995+ def get_pricelist(self, cr, uid, requisition_id, context=None):
996+ """Retrive pricelist id to use in sourcing by agreement process
997+
998+ :returns: pricelist record
999+
1000+ """
1001+ if isinstance(requisition_id, (list, tuple)):
1002+ assert len(requisition_id) == 1
1003+ requisition_id = requisition_id[0]
1004+ requisiton = self.browse(cr, uid, requisition_id, context=context)
1005+ plist = requisiton.partner_id.property_product_pricelist
1006+ if not plist:
1007+ raise orm.except_orm(_('No price list on customer'),
1008+ _('Please set sale price list on %s partner') %
1009+ requisiton.partner_id.name)
1010+ return plist
1011
1012=== added file 'framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py'
1013--- framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py 1970-01-01 00:00:00 +0000
1014+++ framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py 2014-02-06 10:43:21 +0000
1015@@ -0,0 +1,136 @@
1016+# -*- coding: utf-8 -*-
1017+##############################################################################
1018+#
1019+# Author: Nicolas Bessi
1020+# Copyright 2013 Camptocamp SA
1021+#
1022+# This program is free software: you can redistribute it and/or modify
1023+# it under the terms of the GNU Affero General Public License as
1024+# published by the Free Software Foundation, either version 3 of the
1025+# License, or (at your option) any later version.
1026+#
1027+# This program is distributed in the hope that it will be useful,
1028+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1029+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1030+# GNU Affero General Public License for more details.
1031+#
1032+# You should have received a copy of the GNU Affero General Public License
1033+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1034+#
1035+##############################################################################
1036+from openerp.osv import orm
1037+from openerp.tools.translate import _
1038+from .logistic_requisition_source import AGR_PROC
1039+
1040+
1041+class logistic_requisition_cost_estimate(orm.Model):
1042+ """Add update of agreement price"""
1043+
1044+ _inherit = "logistic.requisition.cost.estimate"
1045+
1046+ def _update_agreement_source(self, cr, uid, source, context=None):
1047+ """Update price of source line using related confirmed PO"""
1048+ if source.procurement_method == AGR_PROC:
1049+ self._link_po_lines_to_source(cr, uid, source, context=context)
1050+ price = source.get_agreement_price_from_po()
1051+ source.write({'unit_cost': price})
1052+ source.refresh()
1053+
1054+ def _link_po_lines_to_source(self, cr, uid, source, context=None):
1055+ po_l_obj = self.pool['purchase.order.line']
1056+ agr = source.framework_agreement_id
1057+ line_ids = po_l_obj.search(
1058+ cr, uid,
1059+ [('order_id.framework_agreement_id', '=', agr.id),
1060+ ('lr_source_line_id', '=', source.id),
1061+ ('order_id.partner_id', '=', agr.supplier_id.id)],
1062+ context=context)
1063+ lines = po_l_obj.browse(cr, uid, line_ids, context=context)
1064+ po_ids = ([line.order_id.id for line in lines
1065+ if line.product_id and line.product_id.type == 'product'])
1066+
1067+ all_line_ids = po_l_obj.search(
1068+ cr, uid,
1069+ [('order_id', 'in', po_ids),
1070+ ('product_id.type', '=', 'product')],
1071+ context=context)
1072+
1073+ po_l_obj.write(cr, uid, all_line_ids,
1074+ {'lr_source_line_id': source.id},
1075+ context=context)
1076+ source.refresh()
1077+
1078+ def _prepare_cost_estimate_line(self, cr, uid, sourcing, context=None):
1079+ """Override in order to update agreement source line
1080+
1081+ We update the price of source line that will be used in cost estimate
1082+
1083+ """
1084+ self._update_agreement_source(cr, uid, sourcing, context=context)
1085+ res = super(logistic_requisition_cost_estimate,
1086+ self)._prepare_cost_estimate_line(cr, uid, sourcing,
1087+ context=context)
1088+
1089+ if sourcing.procurement_method == AGR_PROC:
1090+ res['type'] = 'make_to_order'
1091+ res['sale_flow'] = 'direct_delivery'
1092+ return res
1093+
1094+ def _link_po_lines_to_so_lines(self, cr, uid, so, sources, context=None):
1095+ """Naive implementation to link all PO lines to SO lines.
1096+
1097+ For our actuall need we want to link all service line
1098+ to SO real product lines.
1099+
1100+ There should not be twice the same product on differents
1101+ Agreement PO line so this case in not handled
1102+
1103+ """
1104+
1105+ so_lines = [x for x in so.order_line]
1106+ po_lines = set(x.purchase_line_id for x in sources
1107+ if x.purchase_line_id and
1108+ x.purchase_line_id.product_id.type == 'product')
1109+ product_dict = dict((x.product_id.id, x.id) for x in so_lines
1110+ if x.product_id and x.product_id.type == 'product')
1111+ default = product_dict[product_dict.keys()[0]]
1112+ if not product_dict:
1113+ raise orm.except_orm(_('No stockable product in related PO'),
1114+ _('Please add one'))
1115+ for po_line in po_lines:
1116+ key = po_line.product_id.id if po_line.product_id else False
1117+ po_line.write({'sale_order_line_id': product_dict.get(key, default)})
1118+
1119+ def cost_estimate(self, cr, uid, ids, context=None):
1120+ """Override to link PO to cost_estimate
1121+
1122+ We have to do this because when we source with agreement we do
1123+ not copy the PO it is meaningless has we have no choice to make.
1124+ But in tender flow you first cancel PO then the sale order mark
1125+ canceled PO as dropshipping and then copy them.
1126+
1127+ So you have to create link between SO and PO/PO line that are
1128+ normally done when SO procurement generate PO and picking
1129+
1130+
1131+ With agreement PO is confirmed before be marked as dropshipping.
1132+
1133+ So we have to link it first"""
1134+ so_model = self.pool['sale.order']
1135+ po_model = self.pool['purchase.order']
1136+ res = super(logistic_requisition_cost_estimate,
1137+ self).cost_estimate(cr, uid, ids, context=context)
1138+ so_id = res['res_id']
1139+ order = so_model.browse(cr, uid, so_id, context=context)
1140+ # Can be optimized with a SQL or a search but
1141+ # gain of perfo will not worth readability loss
1142+ # for such small data set
1143+ sources = [x.logistic_requisition_source_id for x in order.order_line
1144+ if x and x.logistic_requisition_source_id.procurement_method == AGR_PROC]
1145+ po_ids = set(x.purchase_line_id.order_id.id for x in sources
1146+ if x.purchase_line_id)
1147+ po_model.write(cr, uid, list(po_ids),
1148+ {'sale_id': so_id,
1149+ 'sale_flow': 'direct_delivery'})
1150+ self._link_po_lines_to_so_lines(cr, uid, order, sources, context=context)
1151+ return res
1152
1153=== added file 'framework_agreement_sourcing/model/logistic_requisition_source.py'
1154--- framework_agreement_sourcing/model/logistic_requisition_source.py 1970-01-01 00:00:00 +0000
1155+++ framework_agreement_sourcing/model/logistic_requisition_source.py 2014-02-06 10:43:21 +0000
1156@@ -0,0 +1,369 @@
1157+# -*- coding: utf-8 -*-
1158+##############################################################################
1159+#
1160+# Author: Nicolas Bessi
1161+# Copyright 2013 Camptocamp SA
1162+#
1163+# This program is free software: you can redistribute it and/or modify
1164+# it under the terms of the GNU Affero General Public License as
1165+# published by the Free Software Foundation, either version 3 of the
1166+# License, or (at your option) any later version.
1167+#
1168+# This program is distributed in the hope that it will be useful,
1169+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1170+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1171+# GNU Affero General Public License for more details.
1172+#
1173+# You should have received a copy of the GNU Affero General Public License
1174+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1175+#
1176+##############################################################################
1177+from openerp.osv import orm, fields
1178+from openerp.tools.translate import _
1179+from openerp.addons.framework_agreement.model.framework_agreement import\
1180+ FrameworkAgreementObservable
1181+from openerp.addons.framework_agreement.utils import id_boilerplate
1182+
1183+from .adapter_util import BrowseAdapterMixin, BrowseAdapterSourceMixin
1184+
1185+AGR_PROC = 'fw_agreement'
1186+
1187+
1188+class logistic_requisition_source(orm.Model, BrowseAdapterMixin,
1189+ BrowseAdapterSourceMixin, FrameworkAgreementObservable):
1190+ """Adds support of framework agreement to source line"""
1191+
1192+ _inherit = "logistic.requisition.source"
1193+
1194+ _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
1195+ 'Agreement'),
1196+
1197+ 'purchase_pricelist_id': fields.many2one('product.pricelist',
1198+ 'Purchase (PO) Pricelist',
1199+ help="This pricelist will be used"
1200+ " when generating PO"),
1201+ 'pricelist_id': fields.related('requisition_line_id', 'requisition_id',
1202+ 'partner_id',
1203+ 'property_product_pricelist',
1204+ relation='product.pricelist',
1205+ type='many2one',
1206+ string='Price list',
1207+ readonly=True),
1208+
1209+ 'supplier_id': fields.related('framework_agreement_id', 'supplier_id',
1210+ type='many2one', relation='res.partner',
1211+ string='Agreement Supplier')}
1212+
1213+ def _get_procur_method_hook(self, cr, uid, context=None):
1214+ """Adds framework agreement as a procurement method in selection field"""
1215+ res = super(logistic_requisition_source, self)._get_procur_method_hook(cr, uid,
1216+ context=context)
1217+ res.append((AGR_PROC, 'Framework agreement'))
1218+ return res
1219+
1220+ def _get_purchase_line_id(self, cr, uid, ids, field_name, arg, context=None):
1221+ """For each source line, get the related purchase order line
1222+
1223+ For more detail please refer to function fields documentation
1224+
1225+ """
1226+ po_line_model = self.pool['purchase.order.line']
1227+ res = super(logistic_requisition_source, self)._get_purchase_line_id(cr, uid, ids,
1228+ field_name,
1229+ arg,
1230+ context=context)
1231+ for line in self.browse(cr, uid, ids, context=context):
1232+ if line.procurement_method == AGR_PROC:
1233+ po_l_ids = po_line_model.search(cr, uid,
1234+ [('lr_source_line_id', '=', line.id),
1235+ ('state', '!=', 'cancel')],
1236+ context=context)
1237+ if po_l_ids:
1238+ if len(po_l_ids) > 1:
1239+ raise orm.except_orm(_('Many Purchase order lines found for %s') % line.name,
1240+ _('Please cancel uneeded one'))
1241+ res[line.id] = po_l_ids[0]
1242+ else:
1243+ res[line.id] = False
1244+ return res
1245+
1246+ #------------------ adapting source line to po -----------------------------
1247+
1248+ def _map_source_to_po(self, cr, uid, line, context=None, **kwargs):
1249+ """Map source line to dict to be used by PO create defaults are optional
1250+
1251+ :returns: data dict to be used by adapter
1252+
1253+ """
1254+ supplier = line.framework_agreement_id.supplier_id
1255+ add = line.requisition_id.consignee_shipping_id
1256+ term = supplier.property_supplier_payment_term
1257+ term = term.id if term else False
1258+ position = supplier.property_account_position
1259+ position = position.id if position else False
1260+ requisition = line.requisition_id
1261+ data = {}
1262+ data['framework_agreement_id'] = line.framework_agreement_id.id
1263+ data['partner_id'] = supplier.id
1264+ data['company_id'] = self._company(cr, uid, context)
1265+ data['pricelist_id'] = line.purchase_pricelist_id.id
1266+ data['dest_address_id'] = add.id
1267+ data['location_id'] = add.property_stock_customer.id
1268+ data['payment_term_id'] = term
1269+ data['fiscal_position'] = position
1270+ data['origin'] = requisition.name
1271+ data['date_order'] = requisition.date
1272+ # data['name'] = requisition.name
1273+ data['consignee_id'] = requisition.consignee_id.id
1274+ data['incoterm_id'] = requisition.incoterm_id.id
1275+ data['incoterm_address'] = requisition.incoterm_address
1276+ data['type'] = 'purchase'
1277+ return data
1278+
1279+ def _map_source_to_po_line(self, cr, uid, line, context=None, **kwargs):
1280+ """Map source line to dict to be used by PO line create
1281+ Map source line to dict to be used by PO create
1282+ defaults are optional
1283+
1284+ :returns: data dict to be used by adapter
1285+
1286+ """
1287+ acc_pos_obj = self.pool['account.fiscal.position']
1288+ supplier = line.framework_agreement_id.supplier_id
1289+ taxes_ids = line.proposed_product_id.supplier_taxes_id
1290+ taxes = acc_pos_obj.map_tax(cr, uid, supplier.property_account_position,
1291+ taxes_ids)
1292+ currency = line.purchase_pricelist_id.currency_id
1293+ price = line.framework_agreement_id.get_price(line.proposed_qty, currency=currency)
1294+ lead_time = line.framework_agreement_id.delay
1295+ data = {}
1296+ direct_map = {'product_qty': 'proposed_qty',
1297+ 'product_id': 'proposed_product_id',
1298+ 'product_uom': 'proposed_uom_id',
1299+ 'lr_source_line_id': 'id',
1300+ }
1301+
1302+ data.update(self._direct_map(line, direct_map))
1303+ data['product_lead_time'] = lead_time
1304+ data['price_unit'] = price
1305+ data['name'] = line.proposed_product_id.name
1306+ data['date_planned'] = line.requisition_id.date_delivery
1307+ data['taxes_id'] = [(6, 0, taxes)]
1308+ return data
1309+
1310+ def _make_po_from_source_line(self, cr, uid, source_line, context=None):
1311+ """adapt a source line to purchase order
1312+
1313+ :returns: generated PO id
1314+
1315+ """
1316+ if context is None:
1317+ context = {}
1318+ context['draft_po'] = True
1319+ po_obj = self.pool['purchase.order']
1320+ pid = po_obj._make_purchase_order_from_origin(cr, uid, source_line,
1321+ self._map_source_to_po,
1322+ self._map_source_to_po_line,
1323+ context=context)
1324+
1325+ return pid
1326+
1327+ def make_purchase_order(self, cr, uid, ids, context=None):
1328+ """ adapt each source line to purchase order
1329+
1330+ :returns: generated PO ids
1331+
1332+ """
1333+ po_ids = []
1334+ for source_line in self.browse(cr, uid, ids, context=context):
1335+ po_id = self._make_po_from_source_line(cr, uid, source_line, context=None)
1336+ po_ids.append(po_id)
1337+ return po_ids
1338+
1339+ def action_create_agreement_po_requisition(self, cr, uid, ids, context=None):
1340+ """ Implement buttons that create PO from selected source lines"""
1341+ # We force empty context
1342+ act_obj = self.pool.get('ir.actions.act_window')
1343+ po_ids = self.make_purchase_order(cr, uid, ids, context=context)
1344+ res = act_obj.for_xml_id(cr, uid,
1345+ 'purchase', 'purchase_rfq', context=context)
1346+ res.update({'domain': [('id', 'in', po_ids)],
1347+ 'res_id': False,
1348+ 'context': '{}',
1349+ })
1350+ return res
1351+
1352+ def _is_sourced_fw_agreement(self, cr, uid, source, context=None):
1353+ """Predicate that tells if source line of type agreement are sourced
1354+
1355+ :retuns: boolean True if sourced
1356+
1357+ """
1358+ po_line_obj = self.pool['purchase.order.line']
1359+ sources_ids = po_line_obj.search(cr, uid, [('lr_source_line_id', '=', source.id)],
1360+ context=context)
1361+ # predicate
1362+ return bool(sources_ids)
1363+
1364+ def get_agreement_price_from_po(self, cr, uid, source_id, context=None):
1365+ """Get price from PO.
1366+
1367+ The price is retreived on the po line generated by sourced line.
1368+
1369+ :returns: price in float
1370+ """
1371+ if isinstance(source_id, (list, tuple)):
1372+ assert len(source_id) == 1
1373+ source_id = source_id[0]
1374+ po_l_obj = self.pool['purchase.order.line']
1375+ currency_obj = self.pool['res.currency']
1376+ current = self.browse(cr, uid, source_id, context=context)
1377+ agreement = current.framework_agreement_id
1378+
1379+ if not agreement:
1380+ raise ValueError('No framework agreement on source line %s' %
1381+ current.name)
1382+ line_ids = po_l_obj.search(cr, uid,
1383+ [('order_id.framework_agreement_id', '=', agreement.id),
1384+ ('lr_source_line_id', '=', current.id),
1385+ ('order_id.partner_id', '=', agreement.supplier_id.id)],
1386+ context=context)
1387+ price = 0.0
1388+ lines = po_l_obj.browse(cr, uid, line_ids, context=context)
1389+ if lines:
1390+ price = sum(x.price_subtotal for x in lines) # To avoid rounding problems
1391+ from_curr = lines[0].order_id.pricelist_id.currency_id.id
1392+ to_curr = current.pricelist_id.currency_id.id
1393+ price = currency_obj.compute(cr, uid, from_curr, to_curr, price, False)
1394+ return price
1395+
1396+ #---------------------- provide adapter middleware -------------------------
1397+
1398+ def _make_source_line_from_origin(self, cr, uid, origin, map_fun,
1399+ post_fun=None, context=None, **kwargs):
1400+ model = self.pool['logistic.requisition.source']
1401+ data = self._adapt_origin(cr, uid, model, origin, map_fun,
1402+ post_fun=post_fun, context=context, **kwargs)
1403+ self._validate_adapted_data(cr, uid, model, data, context=context)
1404+ s_id = self.create(cr, uid, data, context=context)
1405+ if callable(post_fun):
1406+ post_fun(cr, uid, s_id, origin, context=context, **kwargs)
1407+ return s_id
1408+
1409+ #---------------OpenERP tedious onchange management ------------------------
1410+
1411+ def _get_date(self, cr, uid, requision_line_id, context=None):
1412+ """helper to retrive date to be used by framework agreement
1413+ when in source line context
1414+
1415+ :param source_id: requisition.line.source id that should
1416+ provide date
1417+
1418+ :returns: date/datetime string
1419+
1420+ """
1421+ req_obj = self.pool['logistic.requisition.line']
1422+ current = req_obj.browse(cr, uid, requision_line_id, context=context)
1423+ now = fields.datetime.now()
1424+ return current.requisition_id.date or now
1425+
1426+ @id_boilerplate
1427+ def onchange_sourcing_method(self, cr, uid, source_id, method, req_line_id, proposed_product_id,
1428+ pricelist_id, proposed_qty=0, context=None):
1429+ """
1430+ Called when source method is set on a source line.
1431+
1432+ If sourcing method is framework agreement
1433+ it will set price, agreement and supplier if possible
1434+ and raise quantity warning.
1435+
1436+ """
1437+ res = {'value': {'framework_agreement_id': False}}
1438+ if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
1439+ return res
1440+ currency = self._currency_get(cr, uid, pricelist_id, context=context)
1441+ agreement_obj = self.pool['framework.agreement']
1442+ date = self._get_date(cr, uid, req_line_id, context=context)
1443+ agreement, enough_qty = agreement_obj.get_cheapest_agreement_for_qty(cr, uid,
1444+ proposed_product_id,
1445+ date,
1446+ proposed_qty,
1447+ currency=currency,
1448+ context=context)
1449+ if not agreement:
1450+ return res
1451+ price = agreement.get_price(proposed_qty, currency=currency)
1452+ res['value'] = {'framework_agreement_id': agreement.id,
1453+ 'unit_cost': price,
1454+ 'total_cost': price * proposed_qty,
1455+ 'supplier_id': agreement.supplier_id.id}
1456+ if not enough_qty:
1457+ msg = _("You have ask for a quantity of %s \n"
1458+ " but there is only %s available"
1459+ " for current agreement") % (proposed_qty, agreement.available_quantity)
1460+ res['warning'] = msg
1461+ return res
1462+
1463+ @id_boilerplate
1464+ def onchange_pricelist(self, cr, uid, source_id, method, req_line_id,
1465+ proposed_product_id, proposed_qty,
1466+ pricelist_id, context=None):
1467+ """Call when pricelist is set on a source line.
1468+
1469+ If sourcing method is framework agreement
1470+ it will set price, agreement and supplier if possible
1471+ and raise quantity warning.
1472+
1473+ """
1474+ res = {}
1475+ if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
1476+ return res
1477+
1478+ return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
1479+ proposed_product_id, pricelist_id,
1480+ proposed_qty=proposed_qty,
1481+ context=context)
1482+
1483+ @id_boilerplate
1484+ def onchange_quantity(self, cr, uid, source_id, method, req_line_id, qty,
1485+ proposed_product_id, pricelist_id, context=None):
1486+ """Raise a warning if agreed qty is not sufficient"""
1487+ if (method != AGR_PROC or not proposed_product_id):
1488+ return {}
1489+ currency = self._currency_get(cr, uid, pricelist_id, context=context)
1490+ date = self._get_date(cr, uid, req_line_id, context=context)
1491+ return self.onchange_quantity_obs(cr, uid, source_id, qty, date,
1492+ proposed_product_id,
1493+ currency=currency,
1494+ price_field='dummy',
1495+ context=context)
1496+
1497+ @id_boilerplate
1498+ def onchange_product_id(self, cr, uid, source_id, method, req_line_id,
1499+ proposed_product_id, proposed_qty,
1500+ pricelist_id, context=None):
1501+ """Call when product is set on a source line.
1502+
1503+ If sourcing method is framework agreement
1504+ it will set price, agreement and supplier if possible
1505+ and raise quantity warning.
1506+
1507+ """
1508+ if (method != AGR_PROC or not proposed_product_id):
1509+ return {}
1510+
1511+ return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
1512+ proposed_product_id, pricelist_id,
1513+ proposed_qty=proposed_qty,
1514+ context=context)
1515+
1516+ @id_boilerplate
1517+ def onchange_agreement(self, cr, uid, source_id, agreement_id, req_line_id, qty,
1518+ proposed_product_id, pricelist_id, context=None):
1519+ if not proposed_product_id or not pricelist_id or not agreement_id:
1520+ return {}
1521+ currency = self._currency_get(cr, uid, pricelist_id, context=context)
1522+ date = self._get_date(cr, uid, req_line_id, context=context)
1523+ return self.onchange_agreement_obs(cr, uid, source_id, agreement_id, qty,
1524+ date, proposed_product_id,
1525+ currency=currency, price_field='dummy')
1526
1527=== added file 'framework_agreement_sourcing/model/purchase.py'
1528--- framework_agreement_sourcing/model/purchase.py 1970-01-01 00:00:00 +0000
1529+++ framework_agreement_sourcing/model/purchase.py 2014-02-06 10:43:21 +0000
1530@@ -0,0 +1,91 @@
1531+# -*- coding: utf-8 -*-
1532+##############################################################################
1533+#
1534+# Author: Nicolas Bessi
1535+# Copyright 2013 Camptocamp SA
1536+#
1537+# This program is free software: you can redistribute it and/or modify
1538+# it under the terms of the GNU Affero General Public License as
1539+# published by the Free Software Foundation, either version 3 of the
1540+# License, or (at your option) any later version.
1541+#
1542+# This program is distributed in the hope that it will be useful,
1543+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1544+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1545+# GNU Affero General Public License for more details.
1546+#
1547+# You should have received a copy of the GNU Affero General Public License
1548+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1549+#
1550+##############################################################################
1551+from openerp.osv import orm
1552+from .adapter_util import BrowseAdapterMixin
1553+
1554+
1555+class purchase_order(orm.Model, BrowseAdapterMixin):
1556+ """Add function to create PO from source line.
1557+ It maybe goes against YAGNI principle.
1558+ The idea would be to propose a small design
1559+ to be ported back into purchase_requisition_extended module
1560+ or an other base modules.
1561+
1562+ Then we should extend it to propose an API
1563+ to generate PO from various sources
1564+ """
1565+
1566+ _inherit = "purchase.order"
1567+
1568+ #------ PO adapter middleware maybe to put in aside class but not easy in OpenERP context ----
1569+ def _make_purchase_order_from_origin(self, cr, uid, origin, map_fun, map_line_fun,
1570+ post_fun=None, post_line_fun=None, context=None):
1571+ """Create a PO browse record from any other record
1572+
1573+ :returns: created record ids
1574+
1575+ """
1576+ po_id = self._adapt_origin_to_po(cr, uid, origin, map_fun,
1577+ post_fun=post_fun, context=context)
1578+ self._adapt_origin_to_po_line(cr, uid, po_id, origin, map_line_fun,
1579+ post_fun=post_line_fun,
1580+ context=context)
1581+ return po_id
1582+
1583+ def _adapt_origin_to_po(self, cr, uid, origin, map_fun,
1584+ post_fun=None, context=None):
1585+ """PO adapter function
1586+
1587+ :returns: created PO id
1588+
1589+ """
1590+ model = self.pool['purchase.order']
1591+ data = self._adapt_origin(cr, uid, model, origin, map_fun,
1592+ post_fun=post_fun, context=context)
1593+ self._validate_adapted_data(cr, uid, model, data, context=context)
1594+ po_id = self.create(cr, uid, data, context=context)
1595+ if callable(post_fun):
1596+ post_fun(cr, uid, po_id, origin, context=context)
1597+ return po_id
1598+
1599+ def _adapt_origin_to_po_line(self, cr, uid, po_id, origin, map_fun,
1600+ post_fun=None, context=None):
1601+ """PO line adapter
1602+
1603+ :returns: created PO line id
1604+
1605+ """
1606+ model = self.pool['purchase.order.line']
1607+ data = self._adapt_origin(cr, uid, model, origin, map_fun,
1608+ post_fun=post_fun, context=context)
1609+ data['order_id'] = po_id
1610+ self._validate_adapted_data(cr, uid, model, data, context=context)
1611+ l_id = model.create(cr, uid, data, context=context)
1612+ if callable(post_fun):
1613+ post_fun(cr, uid, l_id, origin, context=context)
1614+ return l_id
1615+
1616+ def action_confirm(self, cr, uid, ids, context=None):
1617+ super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
1618+ for element in self.browse(cr, uid, ids, context=context):
1619+ if not element.quantity_bid and not element.framework_agreement_id:
1620+ self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
1621+ return True
1622
1623=== added file 'framework_agreement_sourcing/model/sale_order.py'
1624--- framework_agreement_sourcing/model/sale_order.py 1970-01-01 00:00:00 +0000
1625+++ framework_agreement_sourcing/model/sale_order.py 2014-02-06 10:43:21 +0000
1626@@ -0,0 +1,59 @@
1627+# -*- coding: utf-8 -*-
1628+##############################################################################
1629+#
1630+# Author: Nicolas Bessi
1631+# Copyright 2013 Camptocamp SA
1632+#
1633+# This program is free software: you can redistribute it and/or modify
1634+# it under the terms of the GNU Affero General Public License as
1635+# published by the Free Software Foundation, either version 3 of the
1636+# License, or (at your option) any later version.
1637+#
1638+# This program is distributed in the hope that it will be useful,
1639+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1640+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1641+# GNU Affero General Public License for more details.
1642+#
1643+# You should have received a copy of the GNU Affero General Public License
1644+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1645+#
1646+##############################################################################
1647+from openerp import netsvc
1648+from openerp.osv import orm
1649+from .logistic_requisition_source import AGR_PROC
1650+
1651+
1652+class sale_order_line(orm.Model):
1653+ """Pass agreement PO into state confirmed when SO is confirmed"""
1654+
1655+ _inherit = "sale.order.line"
1656+
1657+ def button_confirm(self, cr, uid, ids, context=None):
1658+ """Override confirmation of request of cotation to support LTA
1659+
1660+ Related PO generated by agreement source line will be passed to state confirm.
1661+
1662+ """
1663+ def source_valid(source):
1664+ if source and source.procurement_method == AGR_PROC:
1665+ return True
1666+ return False
1667+ result = super(sale_order_line, self).button_confirm(cr, uid, ids,
1668+ context=context)
1669+ po_line_model = self.pool['purchase.order.line']
1670+ po_model = self.pool['purchase.order']
1671+
1672+ lines = self.browse(cr, uid, ids, context=context)
1673+ source_ids = [x.logistic_requisition_source_id.id for x in lines
1674+ if source_valid(x.logistic_requisition_source_id)]
1675+ po_line_ids = po_line_model.search(cr, uid,
1676+ [('lr_source_line_id', 'in', source_ids)],
1677+ context=context)
1678+ po_lines = po_line_model.read(cr, uid, po_line_ids, ['order_id'],
1679+ load='_classic_write')
1680+ po_ids = set(x['order_id'] for x in po_lines)
1681+ wf_service = netsvc.LocalService("workflow")
1682+ for po in po_model.browse(cr, uid, list(po_ids), context=context):
1683+ wf_service.trg_validate(uid, 'purchase.order', po.id,
1684+ 'draft_po', cr)
1685+ return result
1686
1687=== added directory 'framework_agreement_sourcing/security'
1688=== added directory 'framework_agreement_sourcing/tests'
1689=== added file 'framework_agreement_sourcing/tests/__init__.py'
1690--- framework_agreement_sourcing/tests/__init__.py 1970-01-01 00:00:00 +0000
1691+++ framework_agreement_sourcing/tests/__init__.py 2014-02-06 10:43:21 +0000
1692@@ -0,0 +1,25 @@
1693+# -*- coding: utf-8 -*-
1694+##############################################################################
1695+#
1696+# Author: Nicolas Bessi
1697+# Copyright 2013 Camptocamp SA
1698+#
1699+# This program is free software: you can redistribute it and/or modify
1700+# it under the terms of the GNU Affero General Public License as
1701+# published by the Free Software Foundation, either version 3 of the
1702+# License, or (at your option) any later version.
1703+#
1704+# This program is distributed in the hope that it will be useful,
1705+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1706+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1707+# GNU Affero General Public License for more details.
1708+#
1709+# You should have received a copy of the GNU Affero General Public License
1710+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1711+#
1712+##############################################################################
1713+from . import common
1714+from . import test_logistic_order_line_to_source_line
1715+from . import test_agreement_souce_line_to_po
1716+checks = [test_logistic_order_line_to_source_line,
1717+ test_agreement_souce_line_to_po]
1718
1719=== added file 'framework_agreement_sourcing/tests/common.py'
1720--- framework_agreement_sourcing/tests/common.py 1970-01-01 00:00:00 +0000
1721+++ framework_agreement_sourcing/tests/common.py 2014-02-06 10:43:21 +0000
1722@@ -0,0 +1,143 @@
1723+# -*- coding: utf-8 -*-
1724+##############################################################################
1725+#
1726+# Author: Nicolas Bessi
1727+# Copyright 2013 Camptocamp SA
1728+#
1729+# This program is free software: you can redistribute it and/or modify
1730+# it under the terms of the GNU Affero General Public License as
1731+# published by the Free Software Foundation, either version 3 of the
1732+# License, or (at your option) any later version.
1733+#
1734+# This program is distributed in the hope that it will be useful,
1735+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1736+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1737+# GNU Affero General Public License for more details.
1738+#
1739+# You should have received a copy of the GNU Affero General Public License
1740+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1741+#
1742+##############################################################################
1743+from datetime import timedelta
1744+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
1745+import openerp.tests.common as test_common
1746+from openerp.addons.logistic_requisition.tests import logistic_requisition
1747+from openerp.addons.framework_agreement.tests.common import BaseAgreementTestMixin
1748+
1749+
1750+class CommonSourcingSetUp(test_common.TransactionCase, BaseAgreementTestMixin):
1751+
1752+ def setUp(self):
1753+ """
1754+ Setup a standard configuration for test
1755+ """
1756+ super(CommonSourcingSetUp, self).setUp()
1757+ self.commonsetUp()
1758+ self.requisition_model = self.registry('logistic.requisition')
1759+ self.requisition_line_model = self.registry('logistic.requisition.line')
1760+ self.source_line_model = self.registry('logistic.requisition.source')
1761+ self.make_common_agreements()
1762+ self.make_common_requisition()
1763+
1764+ def make_common_requisition(self):
1765+ """Create a standard logistic requisition"""
1766+ start_date = self.now + timedelta(days=12)
1767+ start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
1768+ req = {
1769+ 'partner_id': self.ref('base.res_partner_1'),
1770+ 'consignee_id': self.ref('base.res_partner_3'),
1771+ 'date_delivery': start_date,
1772+ 'date': start_date,
1773+ 'user_id': self.uid,
1774+ 'budget_holder_id': self.uid,
1775+ 'finance_officer_id': self.uid,
1776+ }
1777+ agr_line = {
1778+ 'product_id': self.product_id,
1779+ 'requested_qty': 100,
1780+ 'requested_uom_id': self.ref('product.product_uom_unit'),
1781+ 'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
1782+ 'budget_tot_price': 100000000,
1783+ }
1784+ other_line = {
1785+ 'product_id': self.ref('product.product_product_7'),
1786+ 'requested_qty': 10,
1787+ 'requested_uom_id': self.ref('product.product_uom_unit'),
1788+ 'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
1789+ 'budget_tot_price': 100000000,
1790+ }
1791+
1792+ requisition_id = logistic_requisition.create(self, req)
1793+ logistic_requisition.add_line(self, requisition_id,
1794+ agr_line)
1795+ logistic_requisition.add_line(self, requisition_id,
1796+ other_line)
1797+ self.requisition = self.requisition_model.browse(self.cr, self.uid, requisition_id)
1798+
1799+ def make_common_agreements(self):
1800+ """Create two default agreements.
1801+
1802+ We have two agreement for same product but using
1803+ different suppliers
1804+
1805+ One supplier has a better price for lower qty the other
1806+ has better price for higher qty
1807+
1808+ We also create one requisition with one line of agreement product
1809+ And one line of other product
1810+
1811+ """
1812+
1813+ cr, uid = self.cr, self.uid
1814+ start_date = self.now + timedelta(days=10)
1815+ start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
1816+ end_date = self.now + timedelta(days=20)
1817+ end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
1818+ # Agreement 1
1819+ agr_id = self.agreement_model.create(cr, uid,
1820+ {'supplier_id': self.supplier_id,
1821+ 'product_id': self.product_id,
1822+ 'start_date': start_date,
1823+ 'end_date': end_date,
1824+ 'draft': False,
1825+ 'delay': 5,
1826+ 'quantity': 2000})
1827+
1828+ pl_id = self.agreement_pl_model.create(cr, uid,
1829+ {'framework_agreement_id': agr_id,
1830+ 'currency_id': self.ref('base.EUR')})
1831+ self.agreement_line_model.create(cr, uid,
1832+ {'framework_agreement_pricelist_id': pl_id,
1833+ 'quantity': 0,
1834+ 'price': 77.0})
1835+
1836+ self.agreement_line_model.create(cr, uid,
1837+ {'framework_agreement_pricelist_id': pl_id,
1838+ 'quantity': 1000,
1839+ 'price': 30.0})
1840+
1841+ self.cheap_on_high_agreement = self.agreement_model.browse(cr, uid, agr_id)
1842+
1843+ # Agreement 2
1844+ agr_id = self.agreement_model.create(cr, uid,
1845+ {'supplier_id': self.ref('base.res_partner_3'),
1846+ 'product_id': self.product_id,
1847+ 'start_date': start_date,
1848+ 'end_date': end_date,
1849+ 'draft': False,
1850+ 'delay': 5,
1851+ 'quantity': 1200})
1852+
1853+ pl_id = self.agreement_pl_model.create(cr, uid,
1854+ {'framework_agreement_id': agr_id,
1855+ 'currency_id': self.ref('base.EUR')})
1856+
1857+ self.agreement_line_model.create(cr, uid,
1858+ {'framework_agreement_pricelist_id': pl_id,
1859+ 'quantity': 0,
1860+ 'price': 50.0})
1861+ self.agreement_line_model.create(cr, uid,
1862+ {'framework_agreement_pricelist_id': pl_id,
1863+ 'quantity': 1000,
1864+ 'price': 45.0})
1865+ self.cheap_on_low_agreement = self.agreement_model.browse(cr, uid, agr_id)
1866
1867=== added file 'framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py'
1868--- framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py 1970-01-01 00:00:00 +0000
1869+++ framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py 2014-02-06 10:43:21 +0000
1870@@ -0,0 +1,74 @@
1871+# -*- coding: utf-8 -*-
1872+##############################################################################
1873+#
1874+# Author: Nicolas Bessi
1875+# Copyright 2013 Camptocamp SA
1876+#
1877+# This program is free software: you can redistribute it and/or modify
1878+# it under the terms of the GNU Affero General Public License as
1879+# published by the Free Software Foundation, either version 3 of the
1880+# License, or (at your option) any later version.
1881+#
1882+# This program is distributed in the hope that it will be useful,
1883+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1884+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1885+# GNU Affero General Public License for more details.
1886+#
1887+# You should have received a copy of the GNU Affero General Public License
1888+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1889+#
1890+##############################################################################
1891+from .common import CommonSourcingSetUp
1892+
1893+
1894+class TestSourceToPo(CommonSourcingSetUp):
1895+
1896+ def setUp(self):
1897+ # we generate a source line
1898+ super(TestSourceToPo, self).setUp()
1899+ cr, uid = self.cr, self.uid
1900+ lines = self.requisition.line_ids
1901+ agr_line = None
1902+ for line in lines:
1903+ if line.product_id == self.cheap_on_low_agreement.product_id:
1904+ agr_line = line
1905+ break
1906+ self.assertTrue(agr_line)
1907+ agr_line.write({'requested_qty': 400})
1908+ agr_line.refresh()
1909+ source_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
1910+ self.assertTrue(len(source_ids) == 1)
1911+ self.source_line = self.source_line_model.browse(cr, uid, source_ids[0])
1912+
1913+ def test_01_transform_source_to_agreement(self):
1914+ """Test transformation of an agreement source line into PO"""
1915+ cr, uid = self.cr, self.uid
1916+ self.assertTrue(self.source_line)
1917+ plist = self.source_line.framework_agreement_id.supplier_id.property_product_pricelist_purchase
1918+ self.source_line.write({'purchase_pricelist_id': plist.id})
1919+ self.source_line.refresh()
1920+ po_id = self.source_line_model._make_po_from_source_line(cr, uid,
1921+ self.source_line)
1922+ self.assertTrue(po_id)
1923+ supplier = self.source_line.framework_agreement_id.supplier_id
1924+ add = self.source_line.requisition_id.consignee_shipping_id
1925+ consignee = self.source_line.requisition_id.consignee_id
1926+ po = self.registry('purchase.order').browse(cr, uid, po_id)
1927+ date_order = self.source_line.requisition_id.date
1928+ date_delivery = self.source_line.requisition_id.date_delivery
1929+ self.assertEqual(po.partner_id, supplier)
1930+ self.assertEqual(po.pricelist_id, supplier.property_product_pricelist_purchase)
1931+ self.assertEqual(po.date_order, date_order)
1932+ self.assertEqual(po.dest_address_id, add)
1933+ self.assertEqual(po.consignee_id, consignee)
1934+ self.assertEqual(po.state, 'draftpo')
1935+
1936+ self.assertEqual(len(po.order_line), 1)
1937+ po_line = po.order_line[0]
1938+ self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
1939+ self.assertEqual(po_line.product_id, self.source_line.proposed_product_id)
1940+ self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
1941+ self.assertEqual(po_line.product_uom, self.source_line.proposed_uom_id)
1942+ self.assertEqual(po_line.price_unit, 50.0)
1943+ self.assertEqual(po_line.lr_source_line_id, self.source_line)
1944+ self.assertEqual(po_line.date_planned, date_delivery)
1945
1946=== added file 'framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py'
1947--- framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py 1970-01-01 00:00:00 +0000
1948+++ framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py 2014-02-06 10:43:21 +0000
1949@@ -0,0 +1,135 @@
1950+# -*- coding: utf-8 -*-
1951+##############################################################################
1952+#
1953+# Author: Nicolas Bessi
1954+# Copyright 2013 Camptocamp SA
1955+#
1956+# This program is free software: you can redistribute it and/or modify
1957+# it under the terms of the GNU Affero General Public License as
1958+# published by the Free Software Foundation, either version 3 of the
1959+# License, or (at your option) any later version.
1960+#
1961+# This program is distributed in the hope that it will be useful,
1962+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1963+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1964+# GNU Affero General Public License for more details.
1965+#
1966+# You should have received a copy of the GNU Affero General Public License
1967+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1968+#
1969+##############################################################################
1970+from ..model.logistic_requisition_source import AGR_PROC
1971+from .common import CommonSourcingSetUp
1972+
1973+
1974+class TestTransformation(CommonSourcingSetUp):
1975+
1976+ def test_01_enough_qty_on_first_agr(self):
1977+ """Test that we can source a line with one agreement and low qty"""
1978+ cr, uid = self.cr, self.uid
1979+ lines = self.requisition.line_ids
1980+ agr_line = None
1981+ for line in lines:
1982+ if line.product_id == self.cheap_on_low_agreement.product_id:
1983+ agr_line = line
1984+ break
1985+ self.assertTrue(agr_line)
1986+ agr_line.write({'requested_qty': 400})
1987+ agr_line.refresh()
1988+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
1989+ self.assertTrue(len(to_validate_ids) == 1)
1990+ to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
1991+ self.assertEqual(to_validate.procurement_method, AGR_PROC)
1992+ self.assertEqual(to_validate.unit_cost, 0.0)
1993+ self.assertEqual(to_validate.proposed_qty, 400)
1994+
1995+ def test_02_enough_qty_on_high_agr(self):
1996+ """Test that we can source a line correctly on both agreement"""
1997+ cr, uid = self.cr, self.uid
1998+ lines = self.requisition.line_ids
1999+ agr_line = None
2000+ for line in lines:
2001+ if line.product_id == self.cheap_on_high_agreement.product_id:
2002+ agr_line = line
2003+ break
2004+ self.assertTrue(agr_line)
2005+ agr_line.write({'requested_qty': 1500})
2006+ agr_line.refresh()
2007+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
2008+ self.assertTrue(len(to_validate_ids) == 1)
2009+ to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
2010+ self.assertEqual(to_validate.procurement_method, AGR_PROC)
2011+ self.assertEqual(to_validate.unit_cost, 0.0)
2012+ self.assertEqual(to_validate.proposed_qty, 1500)
2013+
2014+ def test_03_not_enough_qty_on_high_agreement(self):
2015+ """Test that we can source a line with one agreement and high qty"""
2016+ cr, uid = self.cr, self.uid
2017+ lines = self.requisition.line_ids
2018+ agr_line = None
2019+ for line in lines:
2020+ if line.product_id == self.cheap_on_high_agreement.product_id:
2021+ agr_line = line
2022+ break
2023+ self.assertTrue(agr_line)
2024+ agr_line.write({'requested_qty': 2400})
2025+ agr_line.refresh()
2026+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
2027+ self.assertTrue(len(to_validate_ids) == 2)
2028+ # We validate generated line
2029+ to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
2030+ # high_line
2031+ # idiom taken from Python cookbook
2032+ high_line = next((x for x in to_validates
2033+ if x.framework_agreement_id == self.cheap_on_high_agreement), None)
2034+ self.assertTrue(high_line, msg="High agreement was not used")
2035+ self.assertEqual(high_line.procurement_method, AGR_PROC)
2036+ self.assertEqual(high_line.proposed_qty, 2000)
2037+ self.assertEqual(high_line.unit_cost, 0.0)
2038+
2039+ # low_line
2040+ low_line = next((x for x in to_validates
2041+ if x.framework_agreement_id == self.cheap_on_low_agreement), None)
2042+ self.assertTrue(low_line, msg="Low agreement was not used")
2043+ self.assertEqual(low_line.procurement_method, AGR_PROC)
2044+ self.assertEqual(low_line.proposed_qty, 400)
2045+ self.assertEqual(low_line.unit_cost, 0.0)
2046+
2047+ def test_03_not_enough_qty_on_all_agreemenst(self):
2048+ """Test that we """
2049+ cr, uid = self.cr, self.uid
2050+ lines = self.requisition.line_ids
2051+ agr_line = None
2052+ for line in lines:
2053+ if line.product_id == self.cheap_on_high_agreement.product_id:
2054+ agr_line = line
2055+ break
2056+ self.assertTrue(agr_line)
2057+ agr_line.write({'requested_qty': 5000})
2058+ agr_line.refresh()
2059+ to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
2060+ self.assertTrue(len(to_validate_ids) == 3)
2061+ # We validate generated line
2062+ to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
2063+ # high_line
2064+ # idiom taken from Python cookbook
2065+ high_line = next((x for x in to_validates
2066+ if x.framework_agreement_id == self.cheap_on_high_agreement), None)
2067+ self.assertTrue(high_line, msg="High agreement was not used")
2068+ self.assertEqual(high_line.procurement_method, AGR_PROC)
2069+ self.assertEqual(high_line.proposed_qty, 2000)
2070+ self.assertEqual(high_line.unit_cost, 0.0)
2071+
2072+ # low_line
2073+ low_line = next((x for x in to_validates
2074+ if x.framework_agreement_id == self.cheap_on_low_agreement), None)
2075+ self.assertTrue(low_line, msg="Low agreement was not used")
2076+ self.assertEqual(low_line.procurement_method, AGR_PROC)
2077+ self.assertEqual(low_line.proposed_qty, 1200)
2078+ self.assertEqual(low_line.unit_cost, 0.0)
2079+
2080+ # Tender line
2081+ tender_line = next((x for x in to_validates
2082+ if not x.framework_agreement_id), None)
2083+ self.assertTrue(tender_line, msg="Tender line was not generated")
2084+ self.assertNotEqual(tender_line.procurement_method, AGR_PROC)
2085
2086=== added file 'framework_agreement_sourcing/touch'
2087=== added directory 'framework_agreement_sourcing/view'
2088=== added file 'framework_agreement_sourcing/view/requisition_view.xml'
2089--- framework_agreement_sourcing/view/requisition_view.xml 1970-01-01 00:00:00 +0000
2090+++ framework_agreement_sourcing/view/requisition_view.xml 2014-02-06 10:43:21 +0000
2091@@ -0,0 +1,119 @@
2092+<?xml version="1.0" encoding="utf-8"?>
2093+<openerp>
2094+ <data>
2095+ <record id="add_supplier_and_agreement_on_source_line" model="ir.ui.view">
2096+ <field name="name">add supplier and agrement on source line</field>
2097+ <field name="model">logistic.requisition.source</field>
2098+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
2099+ <field name="arch" type="xml">
2100+ <field name="unit_cost"
2101+ position="before">
2102+ <field name="framework_agreement_id"
2103+ domain="[('draft', '=', False)]"
2104+ attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
2105+ 'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
2106+ on_change="onchange_agreement(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"/>/>
2107+ <field name="pricelist_id"
2108+ invisible="1"/>
2109+ <field name="purchase_pricelist_id"
2110+ attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
2111+ 'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
2112+ on_change="onchange_pricelist(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"
2113+ domain="[('type', '=', 'purchase')]"/>
2114+
2115+ <field name="supplier_id"
2116+ invisible="1"/>
2117+ </field>
2118+ <field name="proposed_uom_id"
2119+ position="attributes">
2120+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
2121+ </field>
2122+ <field name="unit_cost"
2123+ position="attributes">
2124+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
2125+ </field>
2126+ <field name="total_cost"
2127+ position="attributes">
2128+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
2129+ </field>
2130+ <field name="price_is"
2131+ position="attributes">
2132+ <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
2133+ </field>
2134+ </field>
2135+ </record>
2136+
2137+ <record id="add_hide_button_create_bids" model="ir.ui.view">
2138+ <field name="name">hidde button</field>
2139+ <field name="model">logistic.requisition.source</field>
2140+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
2141+ <field name="priority" eval="10"/>
2142+ <field name="arch" type="xml">
2143+ <button position="replace">
2144+ </button>
2145+ </field>
2146+ </record>
2147+
2148+
2149+ <record id="add_create_po_button" model="ir.ui.view">
2150+ <field name="name">add create po button</field>
2151+ <field name="model">logistic.requisition.source</field>
2152+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
2153+ <field name="arch" type="xml">
2154+ <sheet position="before">
2155+ <header>
2156+ <button name="action_create_agreement_po_requisition"
2157+ context="{}"
2158+ string="Create Draft PO"
2159+ type="object"
2160+ attrs="{'invisible': [('procurement_method', '!=', 'fw_agreement')]}"/>
2161+ <button name="action_create_po_requisition"
2162+ string="Call for Bids"
2163+ type="object"
2164+ attrs="{'invisible': ['|', ('po_requisition_id', '!=', False), ('procurement_method', '!=', 'procurement')]}"
2165+ />
2166+ </header>
2167+ </sheet>
2168+ </field>
2169+ </record>
2170+
2171+ <record id="addd_agreement_source_line_onchange" model="ir.ui.view">
2172+ <field name="name">addd agreement source line onchange</field>
2173+ <field name="model">logistic.requisition.source</field>
2174+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
2175+ <field name="arch" type="xml">
2176+ <data>
2177+ <field name="procurement_method"
2178+ position="attributes">
2179+ <attribute name="on_change">onchange_sourcing_method(procurement_method, requisition_line_id, proposed_product_id, purchase_pricelist_id, proposed_qty)</attribute>
2180+ </field>
2181+
2182+ <field name="proposed_qty"
2183+ position="attributes">
2184+ <attribute name="on_change">onchange_quantity(procurement_method, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id)</attribute>
2185+ </field>
2186+ <field name="proposed_product_id"
2187+ position="attributes">
2188+ <attribute name="on_change">onchange_product_id(procurement_method, requisition_line_id, proposed_product_id, proposed_qty, purchase_pricelist_id)</attribute>
2189+ </field>
2190+
2191+ </data>
2192+ </field>
2193+ </record>
2194+
2195+
2196+<!-- Deactivate button on tree to ensure choose of the sourcing method before calling action -->
2197+ <record id="hide_button_on_soure_line in req line tree" model="ir.ui.view">
2198+ <field name="name">hide button on soure line in req line tree</field>
2199+ <field name="model">logistic.requisition.line</field>
2200+ <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_line_form" />
2201+ <field name="arch" type="xml">
2202+ <button name="action_create_po_requisition" position="attributes">
2203+ <attribute name="attrs">{}</attribute>
2204+ <attribute name="invisible">1</attribute>
2205+ </button>
2206+ </field>
2207+ </record>
2208+
2209+ </data>
2210+</openerp>
2211
2212=== modified file 'logistic_requisition/model/logistic_requisition.py'
2213--- logistic_requisition/model/logistic_requisition.py 2013-11-01 09:32:02 +0000
2214+++ logistic_requisition/model/logistic_requisition.py 2014-02-06 10:43:21 +0000
2215@@ -81,7 +81,7 @@
2216 "in charge of the Logistic Requisition"
2217 ),
2218 'partner_id': fields.many2one(
2219- 'res.partner', 'Customer', required=True,
2220+ 'res.partner', 'Customer', required=True, domain=[('customer', '=', True)],
2221 states=REQ_STATES
2222 ),
2223 'consignee_id': fields.many2one(
2224@@ -1090,6 +1090,8 @@
2225 'dest_address_id': dest_address_id,
2226 'line_ids': [(0, 0, rline) for rline in purch_req_lines],
2227 'origin': ", ".join(origin),
2228+ 'req_incoterm_id': line.requisition_id.incoterm_id.id,
2229+ 'req_incoterm_address': line.requisition_id.incoterm_address,
2230 }
2231
2232 def _prepare_po_requisition_line(self, cr, uid, line, context=None):
2233
2234=== modified file 'logistic_requisition/model/purchase.py'
2235--- logistic_requisition/model/purchase.py 2013-09-20 07:12:14 +0000
2236+++ logistic_requisition/model/purchase.py 2014-02-06 10:43:21 +0000
2237@@ -26,6 +26,28 @@
2238 class purchase_order(orm.Model):
2239 _inherit = 'purchase.order'
2240
2241+ def validate_service_product_procurement(self, cr, uid, ids, context=None):
2242+ """ As action_picking_create only take care of non-service product
2243+ by looping on the moves, we need then to pass through all line with
2244+ product of type service and confirm them.
2245+ This way all procurements will reach the done state once the picking
2246+ related to the PO will be done and in the mean while the SO will be
2247+ then marked as delivered.
2248+ """
2249+ wf_service = netsvc.LocalService("workflow")
2250+ proc_obj = self.pool.get('procurement.order')
2251+ # Proc product of type service should be confirm at this
2252+ # stage, otherwise, when picking of related PO is created
2253+ # then done, it stay blocked at running stage
2254+ proc_ids = proc_obj.search(cr, uid, [('purchase_id','in', ids)], context=context)
2255+ for proc in proc_obj.browse(cr, uid, proc_ids, context=context):
2256+ if proc.product_id.type == 'service':
2257+ wf_service.trg_validate(uid, 'procurement.order',
2258+ proc.id, 'button_confirm', cr)
2259+ wf_service.trg_validate(uid, 'procurement.order',
2260+ proc.id, 'button_check', cr)
2261+ return True
2262+
2263 def action_picking_create(self, cr, uid, ids, context=None):
2264 """ When the picking is created, we'll:
2265
2266@@ -62,7 +84,7 @@
2267 if purchase_line is not None:
2268 wf_service.trg_validate(uid, 'procurement.order',
2269 procurement.id, 'button_check', cr)
2270-
2271+ self.validate_service_product_procurement(cr, uid, ids, context)
2272 return picking_id
2273
2274
2275
2276=== modified file 'logistic_requisition/model/sale_order.py'
2277--- logistic_requisition/model/sale_order.py 2013-09-10 11:08:34 +0000
2278+++ logistic_requisition/model/sale_order.py 2014-02-06 10:43:21 +0000
2279@@ -72,7 +72,6 @@
2280 # with it
2281 vals['purchase_id'] = purchase_line.order_id.id
2282 proc_id = proc_obj.create(cr, uid, vals, context=context)
2283-
2284 sale_line.write({'procurement_id': proc_id})
2285 # We do not confirm the procurement. It will stay in 'draft'
2286 # without reservation move. At the moment when the picking
2287@@ -80,6 +79,8 @@
2288 # the id of the picking's move in this procurement and
2289 # confirm the procurement
2290 # (see in purchase_order.action_picking_create())
2291+ # In there, we'll also take care and confirm all procurements
2292+ # with product of type service.
2293
2294 # set the purchases to direct delivery
2295 purchase_obj = self.pool.get('purchase.order')
2296@@ -133,6 +134,14 @@
2297 cr, uid, order, list(lines), picking_id=False, context=context)
2298 return True
2299
2300+ def copy(self, cr, uid, id, default=None, context=None):
2301+ if not default:
2302+ default = {}
2303+ default['invoice_ids'] = False
2304+ default['requisition_id'] = False
2305+ return super(sale_order, self).copy(cr, uid, id,
2306+ default=default, context=context)
2307+
2308
2309 class sale_order_line(orm.Model):
2310 _inherit = "sale.order.line"
2311
2312=== modified file 'logistic_requisition/view/logistic_requisition.xml'
2313--- logistic_requisition/view/logistic_requisition.xml 2013-10-31 09:03:35 +0000
2314+++ logistic_requisition/view/logistic_requisition.xml 2014-02-06 10:43:21 +0000
2315@@ -555,7 +555,7 @@
2316 </group>
2317 <group>
2318 <group string="Purchase Requisition"
2319- attrs="{'invisible': [('procurement_method', 'not in', ['procurement', 'fw_agreement'])]}"
2320+ attrs="{'invisible': [('procurement_method', '!=', 'procurement')]}"
2321 colspan="4">
2322 <label for="po_requisition_id"/>
2323 <div>
2324@@ -571,8 +571,8 @@
2325 colspan="4"
2326 attrs="{'invisible': [('procurement_method', '!=', 'wh_dispatch')]}">
2327 <field name="dispatch_location_id"
2328- on_change="onchange_dispatch_location_id(dispatch_location_id)"
2329- attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}"
2330+ on_change="onchange_dispatch_location_id(dispatch_location_id)"
2331+ attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}"
2332 domain="[('usage', '!=', 'view')]"/>
2333 <field name="stock_owner" />
2334 </group>
2335@@ -624,7 +624,7 @@
2336 <field name="res_model">logistic.requisition.source</field>
2337 <field name="view_type">form</field>
2338 <field name="view_mode">tree,form</field>
2339- <field name="context">{"search_default_groupby_procurement_method" : True}</field>
2340+ <field name="context">{}</field>
2341 <field name="search_view_id" ref="view_logistic_requisition_source_filter"/>
2342 <field name="help"></field>
2343 </record>
2344
2345=== modified file 'logistic_requisition/wizard/cost_estimate.py'
2346--- logistic_requisition/wizard/cost_estimate.py 2013-10-31 15:46:50 +0000
2347+++ logistic_requisition/wizard/cost_estimate.py 2014-02-06 10:43:21 +0000
2348@@ -243,9 +243,9 @@
2349 'incoterm': requisition.incoterm_id.id,
2350 'incoterm_address': requisition.incoterm_address,
2351 'requisition_id': requisition.id,
2352+ 'origin': requisition.name,
2353 'project_id': requisition.analytic_id.id if requisition.analytic_id else False,
2354 }
2355-
2356 onchange_vals = sale_obj.onchange_partner_id(
2357 cr, uid, [], partner_id, context=context).get('value', {})
2358 vals.update(onchange_vals)

Subscribers

People subscribed via source and target branches