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