Merge lp:~camptocamp/openerp-humanitarian-ngo/ngo-addons-add_agreement_sourcing-nbi into lp:~humanitarian-core-editors/openerp-humanitarian-ngo/ngo-addons
- ngo-addons-add_agreement_sourcing-nbi
- Merge into ngo-addons
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 |
Related bugs: |
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 |
Commit message
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
Romain Deheele - Camptocamp (romaindeheele) wrote : | # |
- 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
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_
* The same for the module : framework_
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
- 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
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote : | # |
Hi,
This LGTM, as all the correction have been made here :
So I suggest merging this one first in the trunk and then proposed the *-add-other_
Regards
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote : | # |
LGTM
Preview Diff
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) |
Hello,
Just 2 minor points: agreement_ sourcing/ tests/test_ logistic_ order_line_ to_source_ line.py" is started, not finished...
- an empty file "touch" commited
- last test's docstring in "framework_
Our discussion has clarified all other points. agreement_ tender checkbox when we come from a logistic requisition)
(as another addon planned to hide framework_
Romain