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

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

Description of the change

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

After functional review fixes following points:

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

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

Hello,

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

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

Romain

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

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

119. By Nicolas Bessi - Camptocamp

[FIX] rm dead commented code

120. By Nicolas Bessi - Camptocamp

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

121. By Nicolas Bessi - Camptocamp

[FIX] propagation of incoterm on tender form LR

122. By Nicolas Bessi - Camptocamp

[FIX] propagate framework agreement on purchase order

123. By Nicolas Bessi - Camptocamp

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

124. By Nicolas Bessi - Camptocamp

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

125. By Nicolas Bessi - Camptocamp

[MRG] from head

126. By Nicolas Bessi - Camptocamp

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

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

Hi,

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

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

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

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

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

Regards,

Joël

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

[FIX] constraint to support service in lta flow

128. By Nicolas Bessi - Camptocamp

[IMP] Manifest description

129. By Nicolas Bessi - Camptocamp

[IMP] Manifest description

130. By Nicolas Bessi - Camptocamp

[TYPO]

131. By Nicolas Bessi - Camptocamp

[FIX] invoice name should not be overriden

132. By Nicolas Bessi - Camptocamp

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

133. By Nicolas Bessi - Camptocamp

[FIX] quantity_bid constraint

134. By Nicolas Bessi - Camptocamp

[FIX] LTA po support dropshipping

135. By Nicolas Bessi - Camptocamp

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

136. By Joël Grand-Guillaume @ camptocamp

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

137. By Romain Deheele - Camptocamp

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

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

Hi,

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

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

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

Regards

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

LGTM

review: Approve (code review)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'framework_agreement_requisition'
=== added file 'framework_agreement_requisition/__init__.py'
--- framework_agreement_requisition/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/__init__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,21 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import model
022
=== added file 'framework_agreement_requisition/__openerp__.py'
--- framework_agreement_requisition/__openerp__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/__openerp__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,59 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21{'name': 'Framework Agreement Negociation',
22 'version': '0.1',
23 'author': 'Camptocamp',
24 'maintainer': 'Camptocamp',
25 'category': 'NGO',
26 'complexity': 'normal',
27 'depends': ['purchase_requisition',
28 'purchase_requisition_extended',
29 'framework_agreement'],
30 'description': """
31Negociate framework agreement using tender process
32==================================================
33
34This will allows you to use "The calls for Bids" model
35 to negociate agreement.
36
37To to so you have too check the box "Negociate Agreement".
38
39The module add a state "Agreement selected" on tender and PO.
40
41
42These will be the final state once you have choosen
43the agreement that fit your needs the best.
44
45Once the selection is done juste use the button "Agreement selected" on tender
46That will close flow of tender and related PO accordingly.
47
48""",
49 'website': 'http://www.camptocamp.com',
50 'data': ['requisition_workflow.xml',
51 'purchase_workflow.xml',
52 'view/purchase_requisition_view.xml'],
53 'demo': [],
54 'test': ['test/agreement_requisition.yml'],
55 'installable': True,
56 'auto_install': False,
57 'license': 'AGPL-3',
58 'application': False,
59 }
060
=== added directory 'framework_agreement_requisition/model'
=== added file 'framework_agreement_requisition/model/__init__.py'
--- framework_agreement_requisition/model/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/__init__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import purchase_requisition
22from . import purchase
023
=== added file 'framework_agreement_requisition/model/purchase.py'
--- framework_agreement_requisition/model/purchase.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/purchase.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,106 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp import netsvc
22from openerp.osv import orm
23
24SELECTED_STATE = ('agreement_selected', 'Agreement selected')
25AGR_SELECT = 'agreement_selected'
26
27
28class purchase_order(orm.Model):
29 """Add workflow behavior"""
30
31 _inherit = "purchase.order"
32
33 def __init__(self, pool, cr):
34 """Add a new state value using PO class property"""
35 if SELECTED_STATE not in super(purchase_order, self).STATE_SELECTION:
36 super(purchase_order, self).STATE_SELECTION.append(SELECTED_STATE)
37 return super(purchase_order, self).__init__(pool, cr)
38
39 def select_agreement(self, cr, uid, agr_id, context=None):
40 """Pass PO in state 'Agreement selected'"""
41 if isinstance(agr_id, (list, tuple)):
42 assert len(agr_id) == 1
43 agr_id = agr_id[0]
44 wf_service = netsvc.LocalService("workflow")
45 return wf_service.trg_validate(uid, 'purchase.order',
46 agr_id, 'select_agreement', cr)
47
48 def po_tender_agreement_selected(self, cr, uid, ids, context=None):
49 """Workflow function that write state 'Agreement selected'"""
50 return self.write(cr, uid, ids, {'state': AGR_SELECT},
51 context=context)
52
53
54class purchase_order_line(orm.Model):
55 """Add make_agreement function"""
56
57 _inherit = "purchase.order.line"
58
59 # Did you know a good way to supress SQL constraint to add
60 # Python constraint...
61 _sql_constraints = [
62 ('quantity_bid', 'CHECK(true)',
63 'Selected quantity must be less or equal than the quantity in the bid'),
64 ]
65
66 def _check_quantity_bid(self, cr, uid, ids, context=None):
67 for line in self.browse(cr, uid, ids, context=context):
68 if line.order_id.framework_agreement_id:
69 continue
70 if line.product_id.type == 'product' and not line.quantity_bid <= line.product_qty:
71 return False
72 return True
73
74 _constraints = [
75 (_check_quantity_bid,
76 'Selected quantity must be less or equal than the quantity in the bid',
77 [])
78 ]
79 def _agreement_data(self, cr, uid, po_line, origin, context=None):
80 """Get agreement values from PO line
81
82 :param po_line: Po line records
83
84 :returns: agreement dict to be used by orm.Model.create
85 """
86 vals = {}
87 vals['supplier_id'] = po_line.order_id.partner_id.id
88 vals['product_id'] = po_line.product_id.id
89 vals['quantity'] = po_line.product_qty
90 vals['origin'] = origin if origin else False
91 return vals
92
93 def make_agreement(self, cr, uid, line_id, origin, context=None):
94 """ generate a draft framework agreement
95
96 :returns: a record of LTA
97
98 """
99 agr_model = self.pool['framework.agreement']
100 if isinstance(line_id, (list, tuple)):
101 assert len(line_id) == 1
102 line_id = line_id[0]
103 current = self.browse(cr, uid, line_id, context=context)
104 vals = self._agreement_data(cr, uid, current, origin, context=context)
105 agr_id = agr_model.create(cr, uid, vals, context=context)
106 return agr_model.browse(cr, uid, agr_id, context=context)
0107
=== added file 'framework_agreement_requisition/model/purchase_requisition.py'
--- framework_agreement_requisition/model/purchase_requisition.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/model/purchase_requisition.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,93 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from itertools import chain
22from openerp import netsvc
23from openerp.osv import orm, fields
24from openerp.tools.translate import _
25from .purchase import AGR_SELECT as PO_AGR_SELECT
26
27SELECTED_STATE = ('agreement_selected', 'Agreement selected')
28AGR_SELECT = 'agreement_selected'
29
30
31class purchase_requisition(orm.Model):
32 """Add support to negociate LTA using tender process"""
33
34 def __init__(self, pool, cr):
35 """Nasty hack to add fields to select fields
36
37 We do this in order not to compromising other state added
38 by other addons that are not in inheritance chain...
39
40 """
41 sel = super(purchase_requisition, self)._columns['state']
42 if SELECTED_STATE not in sel.selection:
43 sel.selection.append(SELECTED_STATE)
44 return super(purchase_requisition, self).__init__(pool, cr)
45
46 _inherit = "purchase.requisition"
47 _columns = {
48 'framework_agreement_tender': fields.boolean('Negociate Agreement'),
49 }
50
51 def tender_agreement_selected(self, cr, uid, ids, context=None):
52 """Workflow function that write state 'Agreement selected'"""
53 return self.write(cr, uid, ids, {'state': AGR_SELECT},
54 context=context)
55
56 def select_agreement(self, cr, uid, agr_id, context=None):
57 """Pass tender to state 'Agreement selected'"""
58 if isinstance(agr_id, (list, tuple)):
59 assert len(agr_id) == 1
60 agr_id = agr_id[0]
61 wf_service = netsvc.LocalService("workflow")
62 return wf_service.trg_validate(uid, 'purchase.requisition',
63 agr_id, 'select_agreement', cr)
64
65 def agreement_selected(self, cr, uid, ids, context=None):
66 """Tells tender that an agreement has been selected"""
67 if isinstance(ids, (int, long)):
68 ids = [ids]
69 for req in self.browse(cr, uid, ids, context=context):
70 if not req.framework_agreement_tender:
71 raise orm.except_orm(_('Invalid tender'),
72 _('Request is not of type agreement'))
73 self.select_agreement(cr, uid, req.id, context=context)
74 req.refresh()
75 if req.state != AGR_SELECT:
76 raise RuntimeError('requisiton %s does not pass to state'
77 ' agreement_selected' %
78 req.name)
79 rfqs = chain.from_iterable(req_line.purchase_line_ids
80 for req_line in req.line_ids)
81 rfqs = [rfq for rfq in rfqs if rfq.state == 'confirmed']
82 if not rfqs:
83 raise orm.except_orm(_('No confirmed RFQ related to tender'),
84 _('Please choose at least one'))
85 for rfq in rfqs:
86 rfq.make_agreement(req.name)
87 p_order = rfq.order_id
88 p_order.select_agreement()
89 p_order.refresh()
90 if p_order.state != PO_AGR_SELECT:
91 raise RuntimeError('Purchase order %s does not pass to %' %
92 (p_order.name, PO_AGR_SELECT))
93 return True
094
=== added file 'framework_agreement_requisition/purchase_workflow.xml'
--- framework_agreement_requisition/purchase_workflow.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/purchase_workflow.xml 2014-02-06 10:43:21 +0000
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="act_po_agreement_selected" model="workflow.activity">
5 <field name="wkf_id" ref="purchase.purchase_order"/>
6 <field name="name">Agreement selected</field>
7 <field name="kind">function</field>
8 <field name="action">po_tender_agreement_selected()</field>
9 <field name="flow_stop">True</field>
10 </record>
11 <record id="trans_po_agreement_selected" model="workflow.transition">
12 <field name="act_from" ref="purchase.act_bid"/>
13 <field name="act_to" ref="act_po_agreement_selected"/>
14 <field name="signal">select_agreement</field>
15 </record>
16 </data>
17</openerp>
018
=== added file 'framework_agreement_requisition/requisition_workflow.xml'
--- framework_agreement_requisition/requisition_workflow.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/requisition_workflow.xml 2014-02-06 10:43:21 +0000
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="act_agreement_selected" model="workflow.activity">
5 <field name="wkf_id" ref="purchase_requisition.purchase_requisition_workflow"/>
6 <field name="name">Agreement selected</field>
7 <field name="kind">function</field>
8 <field name="action">tender_agreement_selected()</field>
9 <field name="flow_stop">True</field>
10 </record>
11
12 <record id="trans_agreement_selected" model="workflow.transition">
13 <field name="act_from" ref="purchase_requisition_extended.act_closed"/>
14 <field name="act_to" ref="act_agreement_selected"/>
15 <field name="signal">select_agreement</field>
16 </record>
17 </data>
18</openerp>
019
=== added directory 'framework_agreement_requisition/test'
=== added file 'framework_agreement_requisition/test/agreement_requisition.yml'
--- framework_agreement_requisition/test/agreement_requisition.yml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/test/agreement_requisition.yml 2014-02-06 10:43:21 +0000
@@ -0,0 +1,98 @@
1-
2 Standard flow of a Call for agreement Bids in mode open
3-
4 Create Call for Bids
5-
6 !record {model: purchase.requisition, id: purchase_requisition_agreement}:
7 date_start: '2013-08-02 00:00:00'
8 date_end: '2013-08-30 00:00:00'
9 bid_tendering_mode: 'open'
10 schedule_date: '2013-09-30'
11 req_validity: '2013-09-10'
12 framework_agreement_tender: True
13 line_ids:
14 - product_id: product.product_product_15
15 product_qty: 2500.0
16-
17 Confirm Call
18-
19 !python {model: purchase.requisition}: |
20 import netsvc
21 wf_service = netsvc.LocalService("workflow")
22 wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'sent_suppliers', cr)
23-
24 Create RFQ1. I run the 'Request a quotation' wizard. I fill the supplier.
25-
26 !record {model: purchase.requisition.partner, id: purchase_requisition_agreement_partner1_create}:
27 partner_id: base.res_partner_2
28-
29 Create RFQ1. I confirm the wizard.
30-
31 !python {model: purchase.requisition.partner}: |
32 self.create_order(cr, uid, [ref("purchase_requisition_agreement_partner1_create")],{
33 'active_model': 'purchase.requisition',
34 'active_id': ref("purchase_requisition_agreement"),
35 'active_ids': [ref("purchase_requisition_agreement")],
36 })
37-
38 I encode the bid. I set a 300 price on the line.
39-
40 !python {model: purchase.requisition}: |
41 purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
42 assert len(purchase_req.purchase_ids) == 1, "There must be 1 RFQs linked to this Call for bids"
43 price = 300
44 for rfq in purchase_req.purchase_ids:
45 for line in rfq.order_line:
46 self.pool.get('purchase.order.line').write(cr, uid, [line.id], {'price_unit': price})
47
48-
49 I send the RFQ. For this, I print the RFQ.
50-
51 !python {model: purchase.requisition}: |
52 purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
53 for rfq in purchase_req.purchase_ids:
54 self.pool.get('purchase.order').print_quotation(cr, uid, [rfq.id])
55-
56 I run the 'Bid encoded' wizard of bid1. I fill the date.
57-
58 !record {model: purchase.action_modal_datetime, id: purchase_requisition_agreement_bid1_bidencoded}:
59 datetime: '2013-08-13 00:00:00'
60-
61 I launch wizard action.
62-
63 !python {model: purchase.action_modal_datetime}: |
64 purchase_req = self.pool['purchase.requisition'].browse(cr, uid, ref("purchase_requisition_agreement"))
65 po_id = purchase_req.purchase_ids[0].id
66 self.action(cr, uid, [ref('purchase_requisition_agreement_bid1_bidencoded')],
67 {'action': 'bid_received_ok',
68 'active_id': po_id,
69 'active_ids': [po_id],
70 'active_model': 'purchase.order',
71 'default_datetime': '2013-08-13 00:00:00',
72 'uid': 1})
73-
74 I close the Call for bids and move to bids selection
75-
76 !python {model: purchase.requisition}: |
77 import netsvc
78 wf_service = netsvc.LocalService("workflow")
79 wf_service.trg_validate(uid, 'purchase.requisition', ref("purchase_requisition_agreement"), 'open_bid', cr)
80
81-
82 In the bids selection, I confirm line 1 of bid 1
83-
84 !python {model: purchase.requisition}: |
85 purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
86 self.pool.get('purchase.order.line').action_confirm(cr, uid, [purchase_req.purchase_ids[0].order_line[0].id])
87-
88 I close the call for bids
89-
90 !python {model: purchase.requisition}: |
91 self.close_callforbids(cr, uid, [ref("purchase_requisition_agreement")])
92-
93 I mark the tender as agreement selected
94-
95 !python {model: purchase.requisition}: |
96 self.agreement_selected(cr, uid, ref("purchase_requisition_agreement"))
97 purchase_req = self.browse(cr, uid, ref("purchase_requisition_agreement"))
98 assert purchase_req.state == 'agreement_selected'
099
=== added directory 'framework_agreement_requisition/view'
=== added file 'framework_agreement_requisition/view/purchase_requisition_view.xml'
--- framework_agreement_requisition/view/purchase_requisition_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_requisition/view/purchase_requisition_view.xml 2014-02-06 10:43:21 +0000
@@ -0,0 +1,39 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4 <record model="ir.ui.view" id="view_purchase_requisition_form_agreement">
5 <field name="name">purchase.requisition.form.inherit.aggrement.button</field>
6 <field name="model">purchase.requisition</field>
7 <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_form"/>
8 <field name="arch" type="xml">
9 <field name="multiple_rfq_per_supplier"
10 position="after">
11 <field name="framework_agreement_tender"/>
12 </field>
13 <button name="cancel_requisition" position="after">
14 <button name="agreement_selected"
15 type="object"
16 class="po_buttons oe_form_buttons"
17 attrs="{'invisible': ['|', ('framework_agreement_tender', '=', False), ('state', '!=', 'closed')]}"
18 string="Framework agreement selected"/>
19 </button>
20 </field>
21 </record>
22
23 <record model="ir.ui.view" id="view_purchase_requisition_filter">
24 <field name="name">purchase.requisition.form.inherit.agreement.filter</field>
25 <field name="model">purchase.requisition</field>
26 <field name="inherit_id" ref="purchase_requisition.view_purchase_requisition_filter"/>
27 <field name="arch" type="xml">
28 <filter name="draft"
29 position="after">
30 <filter icon="terp-document-new"
31 name="framework_agreement"
32 string="Framework Agreement?"
33 domain="[(framework_agreement_tender,'=',True)]"/>
34 </filter>
35 </field>
36 </record>
37
38 </data>
39</openerp>
040
=== added directory 'framework_agreement_sourcing'
=== added file 'framework_agreement_sourcing/__init__.py'
--- framework_agreement_sourcing/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/__init__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,21 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import model
022
=== added file 'framework_agreement_sourcing/__openerp__.py'
--- framework_agreement_sourcing/__openerp__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/__openerp__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,55 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21{'name': 'Framework agreement integration in sourcing',
22 'version': '0.1',
23 'author': 'Camptocamp',
24 'maintainer': 'Camptocamp',
25 'category': 'NGO',
26 'complexity': 'normal',
27 'depends': ['framework_agreement', 'logistic_requisition'],
28 'description': """
29Automatically source logistic order from framework agreement
30============================================================
31
32If you have a framework agreement negociated for the current product in
33your logistic requisition. If the date and state of agreement are OK,
34agreement will be used as source for the concerned source lines
35of your request.
36
37In this case tender flow is byassed and confirmed PO will be generated
38when logistic requisition is confirmed.
39
40By default the sourcing process will look in all agreements for a product
41and use them one after the other as long as possible sorted by price.
42
43You can prevent this behavior by forcing only one agreement per product at
44the same time in company.
45
46""",
47 'website': 'http://www.camptocamp.com',
48 'data': ['view/requisition_view.xml'],
49 'demo': [],
50 'test': [],
51 'installable': True,
52 'auto_install': False,
53 'license': 'AGPL-3',
54 'application': False,
55 }
056
=== added directory 'framework_agreement_sourcing/i18n'
=== added directory 'framework_agreement_sourcing/model'
=== added file 'framework_agreement_sourcing/model/__init__.py'
--- framework_agreement_sourcing/model/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/__init__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,26 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import logistic_requisition
22from . import logistic_requisition_source
23from . import purchase
24from . import adapter_util
25from . import sale_order
26from . import logistic_requisition_cost_estimate
027
=== added file 'framework_agreement_sourcing/model/adapter_util.py'
--- framework_agreement_sourcing/model/adapter_util.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/adapter_util.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,116 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21"""Provides basic mechanism to unify the way records are transformed into
22other records
23
24"""
25
26from openerp.osv import orm
27
28
29class BrowseAdapterSourceMixin(object):
30 """Mixin class used by Model that are transformation sources"""
31
32 def _company(self, cr, uid, context):
33 """Return company id
34
35 :returns: company id
36
37 """
38 return self.pool['res.company']._company_default_get(cr, uid, 'purchase.order',
39 context=context)
40
41 def _direct_map(self, line, mapping, context=None):
42 """Take a dict of left key right key and make direct mapping
43 into the model
44
45 :returns: data dict ready to be used
46 """
47 data = {}
48 for po_key, source_key in mapping.iteritems():
49 value = line[source_key]
50 if isinstance(value, orm.browse_record):
51 value = value.id
52 elif isinstance(value, orm.browse_null):
53 value = False
54 elif isinstance(value, orm.browse_record_list):
55 raise NotImplementedError('List are not supported in direct map')
56
57 data[po_key] = value
58 return data
59
60
61class BrowseAdapterMixin(object):
62
63 def _do_checks(self, cr, uid, model, data, context=None):
64 """Perform validation check of adapted data.
65
66 All missing or incorrect values are return at once.
67
68 :returns: array of exceptions
69
70 """
71 required_keys = set(k for k, v in model._columns.iteritems()
72 if v.required and not getattr(v, '_fnct', False))
73 empty_required = set(x for x in data
74 if x in required_keys and not data[x])
75 missing_required = required_keys - set(data.keys())
76 missing_required.update(empty_required)
77 if missing_required:
78 return[ValueError('Following value are missing or False'
79 ' while adapting %s: %s' %
80 (model._name, ", ".join(missing_required)))]
81 return []
82
83 def _validate_adapted_data(self, cr, uid, model, data, context=None):
84 """Perform validation check of adapted data.
85
86 All missing or incorrect values are return at once.
87
88 :returns: validated data or raise Value error
89
90 """
91 errors = self._do_checks(cr, uid, model, data, context=context)
92 if errors:
93 raise ValueError('Data are invalid for following reason %s' %
94 ("\n".join(repr(e) for e in errors)))
95 return data
96
97 def _adapt_origin(self, cr, uid, model, origin,
98 map_fun, post_fun=None, context=None, **kwargs):
99 """Do transformation of source data to dest data using transforms function.
100
101 :param origin: source record
102 :param map_fun: transform function
103 :param post_fun: post transformation hook function
104
105 :returns: transformed data
106
107 """
108 if not callable(map_fun):
109 raise ValueError('Mapping function is not callable')
110 if post_fun and not callable(post_fun):
111 raise ValueError('Post hook function is not callable')
112 data = map_fun(cr, uid, origin, context=context, **kwargs)
113 # we complete with default
114 missing = set(model._columns.keys()) - set(data.keys())
115 data.update(model.default_get(cr, uid, missing, context=context))
116 return data
0117
=== added file 'framework_agreement_sourcing/model/logistic_requisition.py'
--- framework_agreement_sourcing/model/logistic_requisition.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,243 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from collections import namedtuple
22from openerp.tools.translate import _
23from openerp.osv import orm
24from .adapter_util import BrowseAdapterSourceMixin
25from .logistic_requisition_source import AGR_PROC
26
27
28class logistic_requisition_line(orm.Model, BrowseAdapterSourceMixin):
29 """Override to enable generation of source line"""
30
31 _inherit = "logistic.requisition.line"
32
33 def _map_agr_requisiton_to_source(self, cr, uid, line, context=None,
34 qty=0, agreement=None, **kwargs):
35 """Prepare data dict for source line using agreement as source
36
37 :params line: browse record of origin requistion.line
38 :params agreement: browse record of origin agreement
39 :params qty: quantity to be set on source line
40
41 :returns: dict to be used by Model.create
42
43 """
44 res = {}
45 direct_map = {
46 'proposed_product_id': 'product_id',
47 'requisition_line_id': 'id',
48 'proposed_uom_id': 'requested_uom_id'}
49
50 if not agreement:
51 raise ValueError("Missing agreement")
52 if not agreement.product_id.id == line.product_id.id:
53 raise ValueError("Product mismatch for agreement and requisition line")
54 # currency = self._get_source_currency(cr, uid, line, context=context)
55 res['unit_cost'] = 0.0
56 res['proposed_qty'] = qty
57 res['framework_agreement_id'] = agreement.id
58 res['procurement_method'] = AGR_PROC
59 res.update(self._direct_map(line, direct_map))
60 return res
61
62 def _map_requisition_to_source(self, cr, uid, line, context=None,
63 qty=0, **kwargs):
64 """Prepare data dict to generate source line using requisition as source
65
66 :params line: browse record of origin requistion.line
67 :params qty: quantity to be set on source line
68
69 :returns: dict to be used by Model.create
70
71 """
72 res = {}
73 direct_map = {'proposed_product_id': 'product_id',
74 'requisition_line_id': 'id',
75 'proposed_uom_id': 'requested_uom_id'}
76 res['unit_cost'] = 0.0
77 res['proposed_qty'] = qty
78 res['framework_agreement_id'] = False
79 res['procurement_method'] = 'procurement'
80 res.update(self._direct_map(line, direct_map))
81 return res
82
83 def _generate_lines_from_agreements(self, cr, uid, container, line,
84 agreements, qty, currency=None, context=None):
85 """Generate 1/n source line(s) for one requisition line.
86
87 This is done using available agreements.
88 We first look for cheapeast agreement.
89 Then if no more quantity are available and there is still remaining needs
90 we look for next cheapest agreement or return remaining qty
91
92 :param container: list of agreements browse
93 :param qty: quantity to be sourced
94 :param line: origin requisition line
95
96 :returns: remaining quantity to source
97
98 """
99 agreements = agreements if agreements is not None else []
100 if currency:
101 agreements = [x for x in agreements if x.has_currency(currency)]
102 if not agreements:
103 return qty
104 agreements.sort(key=lambda x: x.get_price(qty, currency=currency))
105 current_agr = agreements.pop(0)
106 avail = current_agr.available_quantity
107 if not avail:
108 return qty
109 avail_sold = avail - qty
110 to_consume = qty if avail_sold >= 0 else avail
111
112 source_id = self.make_source_line(cr, uid, line, force_qty=to_consume,
113 agreement=current_agr, context=context)
114 container.append(source_id)
115 difference = qty - to_consume
116 if difference:
117 return self._generate_lines_from_agreements(cr, uid, container, line,
118 agreements, difference, context=context)
119 else:
120 return 0
121
122 def _source_lines_for_agreements(self, cr, uid, line, agreements, currency=None, context=None):
123 """Generate 1/n source line(s) for one requisition line
124
125 This is done using available agreements.
126 We first look for cheapeast agreement.
127 Then if no more quantity are available and there is still remaining needs
128 we look for next cheapest agreement or we create a tender source line
129
130 :param line: requisition line browse record
131 :returns: (generated line ids, remaining qty not covered by agreement)
132
133 """
134 Sourced = namedtuple('Sourced', ['generated', 'remaining'])
135 qty = line.requested_qty
136 generated = []
137 remaining_qty = self._generate_lines_from_agreements(cr, uid, generated,
138 line, agreements, qty,
139 currency=currency, context=context)
140 return Sourced(generated, remaining_qty)
141
142 def make_source_line(self, cr, uid, line, force_qty=None, agreement=None, context=None):
143 """Generate a source line for a tender from a requisition line
144
145 :param line: browse record of origin logistic.request
146 :param force_qty: if set this quantity will be used instead
147 of requested quantity
148 :returns: id of generated source line
149
150 """
151 qty = force_qty if force_qty else line.requested_qty
152 src_obj = self.pool['logistic.requisition.source']
153 if agreement:
154 return src_obj._make_source_line_from_origin(cr, uid, line,
155 self._map_agr_requisiton_to_source,
156 context=context, qty=qty,
157 agreement=agreement)
158 else:
159 return src_obj._make_source_line_from_origin(cr, uid, line,
160 self._map_requisition_to_source,
161 context=context, qty=qty)
162
163 def _get_source_currency(self, cr, uid, line, context=None):
164 agr_obj = self.pool['framework.agreement']
165 comp_obj = self.pool['res.company']
166 currency = line.requisition_id.get_pricelist().currency_id
167 company_id = agr_obj._company_get(cr, uid, context=context)
168 comp_currency = comp_obj.browse(cr, uid, company_id, context=context).currency_id
169 if currency == comp_currency:
170 return None
171 return currency
172
173 def _generate_source_line(self, cr, uid, line, context=None):
174 """Generate one or n source line(s) per requisition line.
175
176 Depending on the available resources. If there is framework agreement(s)
177 running we generate one or n source line using agreements otherwise we generate one
178 source line using tender process
179
180 :param line: browse record of origin logistic.request
181
182 :returns: list of generated source line ids
183
184 """
185 if line.source_ids:
186 return None
187 agr_obj = self.pool['framework.agreement']
188 date = line.requisition_id.date
189 currency = self._get_source_currency(cr, uid, line, context=context)
190 product_id = line.product_id.id
191 agreements = agr_obj.get_all_product_agreements(cr, uid, product_id, date,
192 context=context)
193 generated_lines = []
194 if agreements:
195 line_ids, missing_qty = self._source_lines_for_agreements(cr, uid, line,
196 agreements, currency=currency)
197 generated_lines.extend(line_ids)
198 if missing_qty:
199 generated_lines.append(self.make_source_line(cr, uid, line,
200 force_qty=missing_qty))
201 else:
202 generated_lines.append(self.make_source_line(cr, uid, line))
203
204 return generated_lines
205
206 def _do_confirm(self, cr, uid, ids, context=None):
207 """Override to generate source lines from requision line.
208
209 Please refer to _generate_source_line documentation
210
211 """
212 # TODO refactor
213 # this should probably be in logistic_requisition module
214 # providing a mechanism to allow each type of sourcing method
215 # to generate source line
216 res = super(logistic_requisition_line, self)._do_confirm(cr, uid, ids,
217 context=context)
218 for line_br in self.browse(cr, uid, ids, context=context):
219 self._generate_source_line(cr, uid, line_br, context=context)
220 return res
221
222
223class logistic_requisition(orm.Model):
224 """Add get pricelist function"""
225
226 _inherit = "logistic.requisition"
227
228 def get_pricelist(self, cr, uid, requisition_id, context=None):
229 """Retrive pricelist id to use in sourcing by agreement process
230
231 :returns: pricelist record
232
233 """
234 if isinstance(requisition_id, (list, tuple)):
235 assert len(requisition_id) == 1
236 requisition_id = requisition_id[0]
237 requisiton = self.browse(cr, uid, requisition_id, context=context)
238 plist = requisiton.partner_id.property_product_pricelist
239 if not plist:
240 raise orm.except_orm(_('No price list on customer'),
241 _('Please set sale price list on %s partner') %
242 requisiton.partner_id.name)
243 return plist
0244
=== added file 'framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py'
--- framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition_cost_estimate.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,136 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp.osv import orm
22from openerp.tools.translate import _
23from .logistic_requisition_source import AGR_PROC
24
25
26class logistic_requisition_cost_estimate(orm.Model):
27 """Add update of agreement price"""
28
29 _inherit = "logistic.requisition.cost.estimate"
30
31 def _update_agreement_source(self, cr, uid, source, context=None):
32 """Update price of source line using related confirmed PO"""
33 if source.procurement_method == AGR_PROC:
34 self._link_po_lines_to_source(cr, uid, source, context=context)
35 price = source.get_agreement_price_from_po()
36 source.write({'unit_cost': price})
37 source.refresh()
38
39 def _link_po_lines_to_source(self, cr, uid, source, context=None):
40 po_l_obj = self.pool['purchase.order.line']
41 agr = source.framework_agreement_id
42 line_ids = po_l_obj.search(
43 cr, uid,
44 [('order_id.framework_agreement_id', '=', agr.id),
45 ('lr_source_line_id', '=', source.id),
46 ('order_id.partner_id', '=', agr.supplier_id.id)],
47 context=context)
48 lines = po_l_obj.browse(cr, uid, line_ids, context=context)
49 po_ids = ([line.order_id.id for line in lines
50 if line.product_id and line.product_id.type == 'product'])
51
52 all_line_ids = po_l_obj.search(
53 cr, uid,
54 [('order_id', 'in', po_ids),
55 ('product_id.type', '=', 'product')],
56 context=context)
57
58 po_l_obj.write(cr, uid, all_line_ids,
59 {'lr_source_line_id': source.id},
60 context=context)
61 source.refresh()
62
63 def _prepare_cost_estimate_line(self, cr, uid, sourcing, context=None):
64 """Override in order to update agreement source line
65
66 We update the price of source line that will be used in cost estimate
67
68 """
69 self._update_agreement_source(cr, uid, sourcing, context=context)
70 res = super(logistic_requisition_cost_estimate,
71 self)._prepare_cost_estimate_line(cr, uid, sourcing,
72 context=context)
73
74 if sourcing.procurement_method == AGR_PROC:
75 res['type'] = 'make_to_order'
76 res['sale_flow'] = 'direct_delivery'
77 return res
78
79 def _link_po_lines_to_so_lines(self, cr, uid, so, sources, context=None):
80 """Naive implementation to link all PO lines to SO lines.
81
82 For our actuall need we want to link all service line
83 to SO real product lines.
84
85 There should not be twice the same product on differents
86 Agreement PO line so this case in not handled
87
88 """
89
90 so_lines = [x for x in so.order_line]
91 po_lines = set(x.purchase_line_id for x in sources
92 if x.purchase_line_id and
93 x.purchase_line_id.product_id.type == 'product')
94 product_dict = dict((x.product_id.id, x.id) for x in so_lines
95 if x.product_id and x.product_id.type == 'product')
96 default = product_dict[product_dict.keys()[0]]
97 if not product_dict:
98 raise orm.except_orm(_('No stockable product in related PO'),
99 _('Please add one'))
100 for po_line in po_lines:
101 key = po_line.product_id.id if po_line.product_id else False
102 po_line.write({'sale_order_line_id': product_dict.get(key, default)})
103
104 def cost_estimate(self, cr, uid, ids, context=None):
105 """Override to link PO to cost_estimate
106
107 We have to do this because when we source with agreement we do
108 not copy the PO it is meaningless has we have no choice to make.
109 But in tender flow you first cancel PO then the sale order mark
110 canceled PO as dropshipping and then copy them.
111
112 So you have to create link between SO and PO/PO line that are
113 normally done when SO procurement generate PO and picking
114
115
116 With agreement PO is confirmed before be marked as dropshipping.
117
118 So we have to link it first"""
119 so_model = self.pool['sale.order']
120 po_model = self.pool['purchase.order']
121 res = super(logistic_requisition_cost_estimate,
122 self).cost_estimate(cr, uid, ids, context=context)
123 so_id = res['res_id']
124 order = so_model.browse(cr, uid, so_id, context=context)
125 # Can be optimized with a SQL or a search but
126 # gain of perfo will not worth readability loss
127 # for such small data set
128 sources = [x.logistic_requisition_source_id for x in order.order_line
129 if x and x.logistic_requisition_source_id.procurement_method == AGR_PROC]
130 po_ids = set(x.purchase_line_id.order_id.id for x in sources
131 if x.purchase_line_id)
132 po_model.write(cr, uid, list(po_ids),
133 {'sale_id': so_id,
134 'sale_flow': 'direct_delivery'})
135 self._link_po_lines_to_so_lines(cr, uid, order, sources, context=context)
136 return res
0137
=== added file 'framework_agreement_sourcing/model/logistic_requisition_source.py'
--- framework_agreement_sourcing/model/logistic_requisition_source.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/logistic_requisition_source.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,369 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp.osv import orm, fields
22from openerp.tools.translate import _
23from openerp.addons.framework_agreement.model.framework_agreement import\
24 FrameworkAgreementObservable
25from openerp.addons.framework_agreement.utils import id_boilerplate
26
27from .adapter_util import BrowseAdapterMixin, BrowseAdapterSourceMixin
28
29AGR_PROC = 'fw_agreement'
30
31
32class logistic_requisition_source(orm.Model, BrowseAdapterMixin,
33 BrowseAdapterSourceMixin, FrameworkAgreementObservable):
34 """Adds support of framework agreement to source line"""
35
36 _inherit = "logistic.requisition.source"
37
38 _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
39 'Agreement'),
40
41 'purchase_pricelist_id': fields.many2one('product.pricelist',
42 'Purchase (PO) Pricelist',
43 help="This pricelist will be used"
44 " when generating PO"),
45 'pricelist_id': fields.related('requisition_line_id', 'requisition_id',
46 'partner_id',
47 'property_product_pricelist',
48 relation='product.pricelist',
49 type='many2one',
50 string='Price list',
51 readonly=True),
52
53 'supplier_id': fields.related('framework_agreement_id', 'supplier_id',
54 type='many2one', relation='res.partner',
55 string='Agreement Supplier')}
56
57 def _get_procur_method_hook(self, cr, uid, context=None):
58 """Adds framework agreement as a procurement method in selection field"""
59 res = super(logistic_requisition_source, self)._get_procur_method_hook(cr, uid,
60 context=context)
61 res.append((AGR_PROC, 'Framework agreement'))
62 return res
63
64 def _get_purchase_line_id(self, cr, uid, ids, field_name, arg, context=None):
65 """For each source line, get the related purchase order line
66
67 For more detail please refer to function fields documentation
68
69 """
70 po_line_model = self.pool['purchase.order.line']
71 res = super(logistic_requisition_source, self)._get_purchase_line_id(cr, uid, ids,
72 field_name,
73 arg,
74 context=context)
75 for line in self.browse(cr, uid, ids, context=context):
76 if line.procurement_method == AGR_PROC:
77 po_l_ids = po_line_model.search(cr, uid,
78 [('lr_source_line_id', '=', line.id),
79 ('state', '!=', 'cancel')],
80 context=context)
81 if po_l_ids:
82 if len(po_l_ids) > 1:
83 raise orm.except_orm(_('Many Purchase order lines found for %s') % line.name,
84 _('Please cancel uneeded one'))
85 res[line.id] = po_l_ids[0]
86 else:
87 res[line.id] = False
88 return res
89
90 #------------------ adapting source line to po -----------------------------
91
92 def _map_source_to_po(self, cr, uid, line, context=None, **kwargs):
93 """Map source line to dict to be used by PO create defaults are optional
94
95 :returns: data dict to be used by adapter
96
97 """
98 supplier = line.framework_agreement_id.supplier_id
99 add = line.requisition_id.consignee_shipping_id
100 term = supplier.property_supplier_payment_term
101 term = term.id if term else False
102 position = supplier.property_account_position
103 position = position.id if position else False
104 requisition = line.requisition_id
105 data = {}
106 data['framework_agreement_id'] = line.framework_agreement_id.id
107 data['partner_id'] = supplier.id
108 data['company_id'] = self._company(cr, uid, context)
109 data['pricelist_id'] = line.purchase_pricelist_id.id
110 data['dest_address_id'] = add.id
111 data['location_id'] = add.property_stock_customer.id
112 data['payment_term_id'] = term
113 data['fiscal_position'] = position
114 data['origin'] = requisition.name
115 data['date_order'] = requisition.date
116 # data['name'] = requisition.name
117 data['consignee_id'] = requisition.consignee_id.id
118 data['incoterm_id'] = requisition.incoterm_id.id
119 data['incoterm_address'] = requisition.incoterm_address
120 data['type'] = 'purchase'
121 return data
122
123 def _map_source_to_po_line(self, cr, uid, line, context=None, **kwargs):
124 """Map source line to dict to be used by PO line create
125 Map source line to dict to be used by PO create
126 defaults are optional
127
128 :returns: data dict to be used by adapter
129
130 """
131 acc_pos_obj = self.pool['account.fiscal.position']
132 supplier = line.framework_agreement_id.supplier_id
133 taxes_ids = line.proposed_product_id.supplier_taxes_id
134 taxes = acc_pos_obj.map_tax(cr, uid, supplier.property_account_position,
135 taxes_ids)
136 currency = line.purchase_pricelist_id.currency_id
137 price = line.framework_agreement_id.get_price(line.proposed_qty, currency=currency)
138 lead_time = line.framework_agreement_id.delay
139 data = {}
140 direct_map = {'product_qty': 'proposed_qty',
141 'product_id': 'proposed_product_id',
142 'product_uom': 'proposed_uom_id',
143 'lr_source_line_id': 'id',
144 }
145
146 data.update(self._direct_map(line, direct_map))
147 data['product_lead_time'] = lead_time
148 data['price_unit'] = price
149 data['name'] = line.proposed_product_id.name
150 data['date_planned'] = line.requisition_id.date_delivery
151 data['taxes_id'] = [(6, 0, taxes)]
152 return data
153
154 def _make_po_from_source_line(self, cr, uid, source_line, context=None):
155 """adapt a source line to purchase order
156
157 :returns: generated PO id
158
159 """
160 if context is None:
161 context = {}
162 context['draft_po'] = True
163 po_obj = self.pool['purchase.order']
164 pid = po_obj._make_purchase_order_from_origin(cr, uid, source_line,
165 self._map_source_to_po,
166 self._map_source_to_po_line,
167 context=context)
168
169 return pid
170
171 def make_purchase_order(self, cr, uid, ids, context=None):
172 """ adapt each source line to purchase order
173
174 :returns: generated PO ids
175
176 """
177 po_ids = []
178 for source_line in self.browse(cr, uid, ids, context=context):
179 po_id = self._make_po_from_source_line(cr, uid, source_line, context=None)
180 po_ids.append(po_id)
181 return po_ids
182
183 def action_create_agreement_po_requisition(self, cr, uid, ids, context=None):
184 """ Implement buttons that create PO from selected source lines"""
185 # We force empty context
186 act_obj = self.pool.get('ir.actions.act_window')
187 po_ids = self.make_purchase_order(cr, uid, ids, context=context)
188 res = act_obj.for_xml_id(cr, uid,
189 'purchase', 'purchase_rfq', context=context)
190 res.update({'domain': [('id', 'in', po_ids)],
191 'res_id': False,
192 'context': '{}',
193 })
194 return res
195
196 def _is_sourced_fw_agreement(self, cr, uid, source, context=None):
197 """Predicate that tells if source line of type agreement are sourced
198
199 :retuns: boolean True if sourced
200
201 """
202 po_line_obj = self.pool['purchase.order.line']
203 sources_ids = po_line_obj.search(cr, uid, [('lr_source_line_id', '=', source.id)],
204 context=context)
205 # predicate
206 return bool(sources_ids)
207
208 def get_agreement_price_from_po(self, cr, uid, source_id, context=None):
209 """Get price from PO.
210
211 The price is retreived on the po line generated by sourced line.
212
213 :returns: price in float
214 """
215 if isinstance(source_id, (list, tuple)):
216 assert len(source_id) == 1
217 source_id = source_id[0]
218 po_l_obj = self.pool['purchase.order.line']
219 currency_obj = self.pool['res.currency']
220 current = self.browse(cr, uid, source_id, context=context)
221 agreement = current.framework_agreement_id
222
223 if not agreement:
224 raise ValueError('No framework agreement on source line %s' %
225 current.name)
226 line_ids = po_l_obj.search(cr, uid,
227 [('order_id.framework_agreement_id', '=', agreement.id),
228 ('lr_source_line_id', '=', current.id),
229 ('order_id.partner_id', '=', agreement.supplier_id.id)],
230 context=context)
231 price = 0.0
232 lines = po_l_obj.browse(cr, uid, line_ids, context=context)
233 if lines:
234 price = sum(x.price_subtotal for x in lines) # To avoid rounding problems
235 from_curr = lines[0].order_id.pricelist_id.currency_id.id
236 to_curr = current.pricelist_id.currency_id.id
237 price = currency_obj.compute(cr, uid, from_curr, to_curr, price, False)
238 return price
239
240 #---------------------- provide adapter middleware -------------------------
241
242 def _make_source_line_from_origin(self, cr, uid, origin, map_fun,
243 post_fun=None, context=None, **kwargs):
244 model = self.pool['logistic.requisition.source']
245 data = self._adapt_origin(cr, uid, model, origin, map_fun,
246 post_fun=post_fun, context=context, **kwargs)
247 self._validate_adapted_data(cr, uid, model, data, context=context)
248 s_id = self.create(cr, uid, data, context=context)
249 if callable(post_fun):
250 post_fun(cr, uid, s_id, origin, context=context, **kwargs)
251 return s_id
252
253 #---------------OpenERP tedious onchange management ------------------------
254
255 def _get_date(self, cr, uid, requision_line_id, context=None):
256 """helper to retrive date to be used by framework agreement
257 when in source line context
258
259 :param source_id: requisition.line.source id that should
260 provide date
261
262 :returns: date/datetime string
263
264 """
265 req_obj = self.pool['logistic.requisition.line']
266 current = req_obj.browse(cr, uid, requision_line_id, context=context)
267 now = fields.datetime.now()
268 return current.requisition_id.date or now
269
270 @id_boilerplate
271 def onchange_sourcing_method(self, cr, uid, source_id, method, req_line_id, proposed_product_id,
272 pricelist_id, proposed_qty=0, context=None):
273 """
274 Called when source method is set on a source line.
275
276 If sourcing method is framework agreement
277 it will set price, agreement and supplier if possible
278 and raise quantity warning.
279
280 """
281 res = {'value': {'framework_agreement_id': False}}
282 if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
283 return res
284 currency = self._currency_get(cr, uid, pricelist_id, context=context)
285 agreement_obj = self.pool['framework.agreement']
286 date = self._get_date(cr, uid, req_line_id, context=context)
287 agreement, enough_qty = agreement_obj.get_cheapest_agreement_for_qty(cr, uid,
288 proposed_product_id,
289 date,
290 proposed_qty,
291 currency=currency,
292 context=context)
293 if not agreement:
294 return res
295 price = agreement.get_price(proposed_qty, currency=currency)
296 res['value'] = {'framework_agreement_id': agreement.id,
297 'unit_cost': price,
298 'total_cost': price * proposed_qty,
299 'supplier_id': agreement.supplier_id.id}
300 if not enough_qty:
301 msg = _("You have ask for a quantity of %s \n"
302 " but there is only %s available"
303 " for current agreement") % (proposed_qty, agreement.available_quantity)
304 res['warning'] = msg
305 return res
306
307 @id_boilerplate
308 def onchange_pricelist(self, cr, uid, source_id, method, req_line_id,
309 proposed_product_id, proposed_qty,
310 pricelist_id, context=None):
311 """Call when pricelist is set on a source line.
312
313 If sourcing method is framework agreement
314 it will set price, agreement and supplier if possible
315 and raise quantity warning.
316
317 """
318 res = {}
319 if (method != AGR_PROC or not proposed_product_id or not pricelist_id):
320 return res
321
322 return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
323 proposed_product_id, pricelist_id,
324 proposed_qty=proposed_qty,
325 context=context)
326
327 @id_boilerplate
328 def onchange_quantity(self, cr, uid, source_id, method, req_line_id, qty,
329 proposed_product_id, pricelist_id, context=None):
330 """Raise a warning if agreed qty is not sufficient"""
331 if (method != AGR_PROC or not proposed_product_id):
332 return {}
333 currency = self._currency_get(cr, uid, pricelist_id, context=context)
334 date = self._get_date(cr, uid, req_line_id, context=context)
335 return self.onchange_quantity_obs(cr, uid, source_id, qty, date,
336 proposed_product_id,
337 currency=currency,
338 price_field='dummy',
339 context=context)
340
341 @id_boilerplate
342 def onchange_product_id(self, cr, uid, source_id, method, req_line_id,
343 proposed_product_id, proposed_qty,
344 pricelist_id, context=None):
345 """Call when product is set on a source line.
346
347 If sourcing method is framework agreement
348 it will set price, agreement and supplier if possible
349 and raise quantity warning.
350
351 """
352 if (method != AGR_PROC or not proposed_product_id):
353 return {}
354
355 return self.onchange_sourcing_method(cr, uid, source_id, method, req_line_id,
356 proposed_product_id, pricelist_id,
357 proposed_qty=proposed_qty,
358 context=context)
359
360 @id_boilerplate
361 def onchange_agreement(self, cr, uid, source_id, agreement_id, req_line_id, qty,
362 proposed_product_id, pricelist_id, context=None):
363 if not proposed_product_id or not pricelist_id or not agreement_id:
364 return {}
365 currency = self._currency_get(cr, uid, pricelist_id, context=context)
366 date = self._get_date(cr, uid, req_line_id, context=context)
367 return self.onchange_agreement_obs(cr, uid, source_id, agreement_id, qty,
368 date, proposed_product_id,
369 currency=currency, price_field='dummy')
0370
=== added file 'framework_agreement_sourcing/model/purchase.py'
--- framework_agreement_sourcing/model/purchase.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/purchase.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,91 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp.osv import orm
22from .adapter_util import BrowseAdapterMixin
23
24
25class purchase_order(orm.Model, BrowseAdapterMixin):
26 """Add function to create PO from source line.
27 It maybe goes against YAGNI principle.
28 The idea would be to propose a small design
29 to be ported back into purchase_requisition_extended module
30 or an other base modules.
31
32 Then we should extend it to propose an API
33 to generate PO from various sources
34 """
35
36 _inherit = "purchase.order"
37
38 #------ PO adapter middleware maybe to put in aside class but not easy in OpenERP context ----
39 def _make_purchase_order_from_origin(self, cr, uid, origin, map_fun, map_line_fun,
40 post_fun=None, post_line_fun=None, context=None):
41 """Create a PO browse record from any other record
42
43 :returns: created record ids
44
45 """
46 po_id = self._adapt_origin_to_po(cr, uid, origin, map_fun,
47 post_fun=post_fun, context=context)
48 self._adapt_origin_to_po_line(cr, uid, po_id, origin, map_line_fun,
49 post_fun=post_line_fun,
50 context=context)
51 return po_id
52
53 def _adapt_origin_to_po(self, cr, uid, origin, map_fun,
54 post_fun=None, context=None):
55 """PO adapter function
56
57 :returns: created PO id
58
59 """
60 model = self.pool['purchase.order']
61 data = self._adapt_origin(cr, uid, model, origin, map_fun,
62 post_fun=post_fun, context=context)
63 self._validate_adapted_data(cr, uid, model, data, context=context)
64 po_id = self.create(cr, uid, data, context=context)
65 if callable(post_fun):
66 post_fun(cr, uid, po_id, origin, context=context)
67 return po_id
68
69 def _adapt_origin_to_po_line(self, cr, uid, po_id, origin, map_fun,
70 post_fun=None, context=None):
71 """PO line adapter
72
73 :returns: created PO line id
74
75 """
76 model = self.pool['purchase.order.line']
77 data = self._adapt_origin(cr, uid, model, origin, map_fun,
78 post_fun=post_fun, context=context)
79 data['order_id'] = po_id
80 self._validate_adapted_data(cr, uid, model, data, context=context)
81 l_id = model.create(cr, uid, data, context=context)
82 if callable(post_fun):
83 post_fun(cr, uid, l_id, origin, context=context)
84 return l_id
85
86 def action_confirm(self, cr, uid, ids, context=None):
87 super(purchase_order_line, self).action_confirm(cr, uid, ids, context=context)
88 for element in self.browse(cr, uid, ids, context=context):
89 if not element.quantity_bid and not element.framework_agreement_id:
90 self.write(cr, uid, ids, {'quantity_bid': element.product_qty}, context=context)
91 return True
092
=== added file 'framework_agreement_sourcing/model/sale_order.py'
--- framework_agreement_sourcing/model/sale_order.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/model/sale_order.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,59 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp import netsvc
22from openerp.osv import orm
23from .logistic_requisition_source import AGR_PROC
24
25
26class sale_order_line(orm.Model):
27 """Pass agreement PO into state confirmed when SO is confirmed"""
28
29 _inherit = "sale.order.line"
30
31 def button_confirm(self, cr, uid, ids, context=None):
32 """Override confirmation of request of cotation to support LTA
33
34 Related PO generated by agreement source line will be passed to state confirm.
35
36 """
37 def source_valid(source):
38 if source and source.procurement_method == AGR_PROC:
39 return True
40 return False
41 result = super(sale_order_line, self).button_confirm(cr, uid, ids,
42 context=context)
43 po_line_model = self.pool['purchase.order.line']
44 po_model = self.pool['purchase.order']
45
46 lines = self.browse(cr, uid, ids, context=context)
47 source_ids = [x.logistic_requisition_source_id.id for x in lines
48 if source_valid(x.logistic_requisition_source_id)]
49 po_line_ids = po_line_model.search(cr, uid,
50 [('lr_source_line_id', 'in', source_ids)],
51 context=context)
52 po_lines = po_line_model.read(cr, uid, po_line_ids, ['order_id'],
53 load='_classic_write')
54 po_ids = set(x['order_id'] for x in po_lines)
55 wf_service = netsvc.LocalService("workflow")
56 for po in po_model.browse(cr, uid, list(po_ids), context=context):
57 wf_service.trg_validate(uid, 'purchase.order', po.id,
58 'draft_po', cr)
59 return result
060
=== added directory 'framework_agreement_sourcing/security'
=== added directory 'framework_agreement_sourcing/tests'
=== added file 'framework_agreement_sourcing/tests/__init__.py'
--- framework_agreement_sourcing/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/__init__.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,25 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import common
22from . import test_logistic_order_line_to_source_line
23from . import test_agreement_souce_line_to_po
24checks = [test_logistic_order_line_to_source_line,
25 test_agreement_souce_line_to_po]
026
=== added file 'framework_agreement_sourcing/tests/common.py'
--- framework_agreement_sourcing/tests/common.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/common.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,143 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from datetime import timedelta
22from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
23import openerp.tests.common as test_common
24from openerp.addons.logistic_requisition.tests import logistic_requisition
25from openerp.addons.framework_agreement.tests.common import BaseAgreementTestMixin
26
27
28class CommonSourcingSetUp(test_common.TransactionCase, BaseAgreementTestMixin):
29
30 def setUp(self):
31 """
32 Setup a standard configuration for test
33 """
34 super(CommonSourcingSetUp, self).setUp()
35 self.commonsetUp()
36 self.requisition_model = self.registry('logistic.requisition')
37 self.requisition_line_model = self.registry('logistic.requisition.line')
38 self.source_line_model = self.registry('logistic.requisition.source')
39 self.make_common_agreements()
40 self.make_common_requisition()
41
42 def make_common_requisition(self):
43 """Create a standard logistic requisition"""
44 start_date = self.now + timedelta(days=12)
45 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
46 req = {
47 'partner_id': self.ref('base.res_partner_1'),
48 'consignee_id': self.ref('base.res_partner_3'),
49 'date_delivery': start_date,
50 'date': start_date,
51 'user_id': self.uid,
52 'budget_holder_id': self.uid,
53 'finance_officer_id': self.uid,
54 }
55 agr_line = {
56 'product_id': self.product_id,
57 'requested_qty': 100,
58 'requested_uom_id': self.ref('product.product_uom_unit'),
59 'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
60 'budget_tot_price': 100000000,
61 }
62 other_line = {
63 'product_id': self.ref('product.product_product_7'),
64 'requested_qty': 10,
65 'requested_uom_id': self.ref('product.product_uom_unit'),
66 'date_delivery': self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
67 'budget_tot_price': 100000000,
68 }
69
70 requisition_id = logistic_requisition.create(self, req)
71 logistic_requisition.add_line(self, requisition_id,
72 agr_line)
73 logistic_requisition.add_line(self, requisition_id,
74 other_line)
75 self.requisition = self.requisition_model.browse(self.cr, self.uid, requisition_id)
76
77 def make_common_agreements(self):
78 """Create two default agreements.
79
80 We have two agreement for same product but using
81 different suppliers
82
83 One supplier has a better price for lower qty the other
84 has better price for higher qty
85
86 We also create one requisition with one line of agreement product
87 And one line of other product
88
89 """
90
91 cr, uid = self.cr, self.uid
92 start_date = self.now + timedelta(days=10)
93 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
94 end_date = self.now + timedelta(days=20)
95 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
96 # Agreement 1
97 agr_id = self.agreement_model.create(cr, uid,
98 {'supplier_id': self.supplier_id,
99 'product_id': self.product_id,
100 'start_date': start_date,
101 'end_date': end_date,
102 'draft': False,
103 'delay': 5,
104 'quantity': 2000})
105
106 pl_id = self.agreement_pl_model.create(cr, uid,
107 {'framework_agreement_id': agr_id,
108 'currency_id': self.ref('base.EUR')})
109 self.agreement_line_model.create(cr, uid,
110 {'framework_agreement_pricelist_id': pl_id,
111 'quantity': 0,
112 'price': 77.0})
113
114 self.agreement_line_model.create(cr, uid,
115 {'framework_agreement_pricelist_id': pl_id,
116 'quantity': 1000,
117 'price': 30.0})
118
119 self.cheap_on_high_agreement = self.agreement_model.browse(cr, uid, agr_id)
120
121 # Agreement 2
122 agr_id = self.agreement_model.create(cr, uid,
123 {'supplier_id': self.ref('base.res_partner_3'),
124 'product_id': self.product_id,
125 'start_date': start_date,
126 'end_date': end_date,
127 'draft': False,
128 'delay': 5,
129 'quantity': 1200})
130
131 pl_id = self.agreement_pl_model.create(cr, uid,
132 {'framework_agreement_id': agr_id,
133 'currency_id': self.ref('base.EUR')})
134
135 self.agreement_line_model.create(cr, uid,
136 {'framework_agreement_pricelist_id': pl_id,
137 'quantity': 0,
138 'price': 50.0})
139 self.agreement_line_model.create(cr, uid,
140 {'framework_agreement_pricelist_id': pl_id,
141 'quantity': 1000,
142 'price': 45.0})
143 self.cheap_on_low_agreement = self.agreement_model.browse(cr, uid, agr_id)
0144
=== added file 'framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py'
--- framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/test_agreement_souce_line_to_po.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,74 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from .common import CommonSourcingSetUp
22
23
24class TestSourceToPo(CommonSourcingSetUp):
25
26 def setUp(self):
27 # we generate a source line
28 super(TestSourceToPo, self).setUp()
29 cr, uid = self.cr, self.uid
30 lines = self.requisition.line_ids
31 agr_line = None
32 for line in lines:
33 if line.product_id == self.cheap_on_low_agreement.product_id:
34 agr_line = line
35 break
36 self.assertTrue(agr_line)
37 agr_line.write({'requested_qty': 400})
38 agr_line.refresh()
39 source_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
40 self.assertTrue(len(source_ids) == 1)
41 self.source_line = self.source_line_model.browse(cr, uid, source_ids[0])
42
43 def test_01_transform_source_to_agreement(self):
44 """Test transformation of an agreement source line into PO"""
45 cr, uid = self.cr, self.uid
46 self.assertTrue(self.source_line)
47 plist = self.source_line.framework_agreement_id.supplier_id.property_product_pricelist_purchase
48 self.source_line.write({'purchase_pricelist_id': plist.id})
49 self.source_line.refresh()
50 po_id = self.source_line_model._make_po_from_source_line(cr, uid,
51 self.source_line)
52 self.assertTrue(po_id)
53 supplier = self.source_line.framework_agreement_id.supplier_id
54 add = self.source_line.requisition_id.consignee_shipping_id
55 consignee = self.source_line.requisition_id.consignee_id
56 po = self.registry('purchase.order').browse(cr, uid, po_id)
57 date_order = self.source_line.requisition_id.date
58 date_delivery = self.source_line.requisition_id.date_delivery
59 self.assertEqual(po.partner_id, supplier)
60 self.assertEqual(po.pricelist_id, supplier.property_product_pricelist_purchase)
61 self.assertEqual(po.date_order, date_order)
62 self.assertEqual(po.dest_address_id, add)
63 self.assertEqual(po.consignee_id, consignee)
64 self.assertEqual(po.state, 'draftpo')
65
66 self.assertEqual(len(po.order_line), 1)
67 po_line = po.order_line[0]
68 self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
69 self.assertEqual(po_line.product_id, self.source_line.proposed_product_id)
70 self.assertEqual(po_line.product_qty, self.source_line.proposed_qty)
71 self.assertEqual(po_line.product_uom, self.source_line.proposed_uom_id)
72 self.assertEqual(po_line.price_unit, 50.0)
73 self.assertEqual(po_line.lr_source_line_id, self.source_line)
74 self.assertEqual(po_line.date_planned, date_delivery)
075
=== added file 'framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py'
--- framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/tests/test_logistic_order_line_to_source_line.py 2014-02-06 10:43:21 +0000
@@ -0,0 +1,135 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from ..model.logistic_requisition_source import AGR_PROC
22from .common import CommonSourcingSetUp
23
24
25class TestTransformation(CommonSourcingSetUp):
26
27 def test_01_enough_qty_on_first_agr(self):
28 """Test that we can source a line with one agreement and low qty"""
29 cr, uid = self.cr, self.uid
30 lines = self.requisition.line_ids
31 agr_line = None
32 for line in lines:
33 if line.product_id == self.cheap_on_low_agreement.product_id:
34 agr_line = line
35 break
36 self.assertTrue(agr_line)
37 agr_line.write({'requested_qty': 400})
38 agr_line.refresh()
39 to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
40 self.assertTrue(len(to_validate_ids) == 1)
41 to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
42 self.assertEqual(to_validate.procurement_method, AGR_PROC)
43 self.assertEqual(to_validate.unit_cost, 0.0)
44 self.assertEqual(to_validate.proposed_qty, 400)
45
46 def test_02_enough_qty_on_high_agr(self):
47 """Test that we can source a line correctly on both agreement"""
48 cr, uid = self.cr, self.uid
49 lines = self.requisition.line_ids
50 agr_line = None
51 for line in lines:
52 if line.product_id == self.cheap_on_high_agreement.product_id:
53 agr_line = line
54 break
55 self.assertTrue(agr_line)
56 agr_line.write({'requested_qty': 1500})
57 agr_line.refresh()
58 to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
59 self.assertTrue(len(to_validate_ids) == 1)
60 to_validate = self.source_line_model.browse(cr, uid, to_validate_ids[0])
61 self.assertEqual(to_validate.procurement_method, AGR_PROC)
62 self.assertEqual(to_validate.unit_cost, 0.0)
63 self.assertEqual(to_validate.proposed_qty, 1500)
64
65 def test_03_not_enough_qty_on_high_agreement(self):
66 """Test that we can source a line with one agreement and high qty"""
67 cr, uid = self.cr, self.uid
68 lines = self.requisition.line_ids
69 agr_line = None
70 for line in lines:
71 if line.product_id == self.cheap_on_high_agreement.product_id:
72 agr_line = line
73 break
74 self.assertTrue(agr_line)
75 agr_line.write({'requested_qty': 2400})
76 agr_line.refresh()
77 to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
78 self.assertTrue(len(to_validate_ids) == 2)
79 # We validate generated line
80 to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
81 # high_line
82 # idiom taken from Python cookbook
83 high_line = next((x for x in to_validates
84 if x.framework_agreement_id == self.cheap_on_high_agreement), None)
85 self.assertTrue(high_line, msg="High agreement was not used")
86 self.assertEqual(high_line.procurement_method, AGR_PROC)
87 self.assertEqual(high_line.proposed_qty, 2000)
88 self.assertEqual(high_line.unit_cost, 0.0)
89
90 # low_line
91 low_line = next((x for x in to_validates
92 if x.framework_agreement_id == self.cheap_on_low_agreement), None)
93 self.assertTrue(low_line, msg="Low agreement was not used")
94 self.assertEqual(low_line.procurement_method, AGR_PROC)
95 self.assertEqual(low_line.proposed_qty, 400)
96 self.assertEqual(low_line.unit_cost, 0.0)
97
98 def test_03_not_enough_qty_on_all_agreemenst(self):
99 """Test that we """
100 cr, uid = self.cr, self.uid
101 lines = self.requisition.line_ids
102 agr_line = None
103 for line in lines:
104 if line.product_id == self.cheap_on_high_agreement.product_id:
105 agr_line = line
106 break
107 self.assertTrue(agr_line)
108 agr_line.write({'requested_qty': 5000})
109 agr_line.refresh()
110 to_validate_ids = self.requisition_line_model._generate_source_line(cr, uid, agr_line)
111 self.assertTrue(len(to_validate_ids) == 3)
112 # We validate generated line
113 to_validates = self.source_line_model.browse(cr, uid, to_validate_ids)
114 # high_line
115 # idiom taken from Python cookbook
116 high_line = next((x for x in to_validates
117 if x.framework_agreement_id == self.cheap_on_high_agreement), None)
118 self.assertTrue(high_line, msg="High agreement was not used")
119 self.assertEqual(high_line.procurement_method, AGR_PROC)
120 self.assertEqual(high_line.proposed_qty, 2000)
121 self.assertEqual(high_line.unit_cost, 0.0)
122
123 # low_line
124 low_line = next((x for x in to_validates
125 if x.framework_agreement_id == self.cheap_on_low_agreement), None)
126 self.assertTrue(low_line, msg="Low agreement was not used")
127 self.assertEqual(low_line.procurement_method, AGR_PROC)
128 self.assertEqual(low_line.proposed_qty, 1200)
129 self.assertEqual(low_line.unit_cost, 0.0)
130
131 # Tender line
132 tender_line = next((x for x in to_validates
133 if not x.framework_agreement_id), None)
134 self.assertTrue(tender_line, msg="Tender line was not generated")
135 self.assertNotEqual(tender_line.procurement_method, AGR_PROC)
0136
=== added file 'framework_agreement_sourcing/touch'
=== added directory 'framework_agreement_sourcing/view'
=== added file 'framework_agreement_sourcing/view/requisition_view.xml'
--- framework_agreement_sourcing/view/requisition_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement_sourcing/view/requisition_view.xml 2014-02-06 10:43:21 +0000
@@ -0,0 +1,119 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="add_supplier_and_agreement_on_source_line" model="ir.ui.view">
5 <field name="name">add supplier and agrement on source line</field>
6 <field name="model">logistic.requisition.source</field>
7 <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
8 <field name="arch" type="xml">
9 <field name="unit_cost"
10 position="before">
11 <field name="framework_agreement_id"
12 domain="[('draft', '=', False)]"
13 attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
14 'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
15 on_change="onchange_agreement(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"/>/>
16 <field name="pricelist_id"
17 invisible="1"/>
18 <field name="purchase_pricelist_id"
19 attrs="{'required': [('procurement_method', '=', 'fw_agreement')],
20 'invisible': [('procurement_method', '!=', 'fw_agreement')]}"
21 on_change="onchange_pricelist(framework_agreement_id, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id, context)"
22 domain="[('type', '=', 'purchase')]"/>
23
24 <field name="supplier_id"
25 invisible="1"/>
26 </field>
27 <field name="proposed_uom_id"
28 position="attributes">
29 <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
30 </field>
31 <field name="unit_cost"
32 position="attributes">
33 <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
34 </field>
35 <field name="total_cost"
36 position="attributes">
37 <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
38 </field>
39 <field name="price_is"
40 position="attributes">
41 <attribute name="attrs">{'invisible': [('procurement_method', '=', 'fw_agreement')]}</attribute>
42 </field>
43 </field>
44 </record>
45
46 <record id="add_hide_button_create_bids" model="ir.ui.view">
47 <field name="name">hidde button</field>
48 <field name="model">logistic.requisition.source</field>
49 <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
50 <field name="priority" eval="10"/>
51 <field name="arch" type="xml">
52 <button position="replace">
53 </button>
54 </field>
55 </record>
56
57
58 <record id="add_create_po_button" model="ir.ui.view">
59 <field name="name">add create po button</field>
60 <field name="model">logistic.requisition.source</field>
61 <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
62 <field name="arch" type="xml">
63 <sheet position="before">
64 <header>
65 <button name="action_create_agreement_po_requisition"
66 context="{}"
67 string="Create Draft PO"
68 type="object"
69 attrs="{'invisible': [('procurement_method', '!=', 'fw_agreement')]}"/>
70 <button name="action_create_po_requisition"
71 string="Call for Bids"
72 type="object"
73 attrs="{'invisible': ['|', ('po_requisition_id', '!=', False), ('procurement_method', '!=', 'procurement')]}"
74 />
75 </header>
76 </sheet>
77 </field>
78 </record>
79
80 <record id="addd_agreement_source_line_onchange" model="ir.ui.view">
81 <field name="name">addd agreement source line onchange</field>
82 <field name="model">logistic.requisition.source</field>
83 <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_source_form"/>
84 <field name="arch" type="xml">
85 <data>
86 <field name="procurement_method"
87 position="attributes">
88 <attribute name="on_change">onchange_sourcing_method(procurement_method, requisition_line_id, proposed_product_id, purchase_pricelist_id, proposed_qty)</attribute>
89 </field>
90
91 <field name="proposed_qty"
92 position="attributes">
93 <attribute name="on_change">onchange_quantity(procurement_method, requisition_line_id, proposed_qty, proposed_product_id, purchase_pricelist_id)</attribute>
94 </field>
95 <field name="proposed_product_id"
96 position="attributes">
97 <attribute name="on_change">onchange_product_id(procurement_method, requisition_line_id, proposed_product_id, proposed_qty, purchase_pricelist_id)</attribute>
98 </field>
99
100 </data>
101 </field>
102 </record>
103
104
105<!-- Deactivate button on tree to ensure choose of the sourcing method before calling action -->
106 <record id="hide_button_on_soure_line in req line tree" model="ir.ui.view">
107 <field name="name">hide button on soure line in req line tree</field>
108 <field name="model">logistic.requisition.line</field>
109 <field name="inherit_id" ref="logistic_requisition.view_logistic_requisition_line_form" />
110 <field name="arch" type="xml">
111 <button name="action_create_po_requisition" position="attributes">
112 <attribute name="attrs">{}</attribute>
113 <attribute name="invisible">1</attribute>
114 </button>
115 </field>
116 </record>
117
118 </data>
119</openerp>
0120
=== modified file 'logistic_requisition/model/logistic_requisition.py'
--- logistic_requisition/model/logistic_requisition.py 2013-11-01 09:32:02 +0000
+++ logistic_requisition/model/logistic_requisition.py 2014-02-06 10:43:21 +0000
@@ -81,7 +81,7 @@
81 "in charge of the Logistic Requisition"81 "in charge of the Logistic Requisition"
82 ),82 ),
83 'partner_id': fields.many2one(83 'partner_id': fields.many2one(
84 'res.partner', 'Customer', required=True,84 'res.partner', 'Customer', required=True, domain=[('customer', '=', True)],
85 states=REQ_STATES85 states=REQ_STATES
86 ),86 ),
87 'consignee_id': fields.many2one(87 'consignee_id': fields.many2one(
@@ -1090,6 +1090,8 @@
1090 'dest_address_id': dest_address_id,1090 'dest_address_id': dest_address_id,
1091 'line_ids': [(0, 0, rline) for rline in purch_req_lines],1091 'line_ids': [(0, 0, rline) for rline in purch_req_lines],
1092 'origin': ", ".join(origin),1092 'origin': ", ".join(origin),
1093 'req_incoterm_id': line.requisition_id.incoterm_id.id,
1094 'req_incoterm_address': line.requisition_id.incoterm_address,
1093 }1095 }
10941096
1095 def _prepare_po_requisition_line(self, cr, uid, line, context=None):1097 def _prepare_po_requisition_line(self, cr, uid, line, context=None):
10961098
=== modified file 'logistic_requisition/model/purchase.py'
--- logistic_requisition/model/purchase.py 2013-09-20 07:12:14 +0000
+++ logistic_requisition/model/purchase.py 2014-02-06 10:43:21 +0000
@@ -26,6 +26,28 @@
26class purchase_order(orm.Model):26class purchase_order(orm.Model):
27 _inherit = 'purchase.order'27 _inherit = 'purchase.order'
2828
29 def validate_service_product_procurement(self, cr, uid, ids, context=None):
30 """ As action_picking_create only take care of non-service product
31 by looping on the moves, we need then to pass through all line with
32 product of type service and confirm them.
33 This way all procurements will reach the done state once the picking
34 related to the PO will be done and in the mean while the SO will be
35 then marked as delivered.
36 """
37 wf_service = netsvc.LocalService("workflow")
38 proc_obj = self.pool.get('procurement.order')
39 # Proc product of type service should be confirm at this
40 # stage, otherwise, when picking of related PO is created
41 # then done, it stay blocked at running stage
42 proc_ids = proc_obj.search(cr, uid, [('purchase_id','in', ids)], context=context)
43 for proc in proc_obj.browse(cr, uid, proc_ids, context=context):
44 if proc.product_id.type == 'service':
45 wf_service.trg_validate(uid, 'procurement.order',
46 proc.id, 'button_confirm', cr)
47 wf_service.trg_validate(uid, 'procurement.order',
48 proc.id, 'button_check', cr)
49 return True
50
29 def action_picking_create(self, cr, uid, ids, context=None):51 def action_picking_create(self, cr, uid, ids, context=None):
30 """ When the picking is created, we'll:52 """ When the picking is created, we'll:
3153
@@ -62,7 +84,7 @@
62 if purchase_line is not None:84 if purchase_line is not None:
63 wf_service.trg_validate(uid, 'procurement.order',85 wf_service.trg_validate(uid, 'procurement.order',
64 procurement.id, 'button_check', cr)86 procurement.id, 'button_check', cr)
6587 self.validate_service_product_procurement(cr, uid, ids, context)
66 return picking_id88 return picking_id
6789
6890
6991
=== modified file 'logistic_requisition/model/sale_order.py'
--- logistic_requisition/model/sale_order.py 2013-09-10 11:08:34 +0000
+++ logistic_requisition/model/sale_order.py 2014-02-06 10:43:21 +0000
@@ -72,7 +72,6 @@
72 # with it72 # with it
73 vals['purchase_id'] = purchase_line.order_id.id73 vals['purchase_id'] = purchase_line.order_id.id
74 proc_id = proc_obj.create(cr, uid, vals, context=context)74 proc_id = proc_obj.create(cr, uid, vals, context=context)
75
76 sale_line.write({'procurement_id': proc_id})75 sale_line.write({'procurement_id': proc_id})
77 # We do not confirm the procurement. It will stay in 'draft'76 # We do not confirm the procurement. It will stay in 'draft'
78 # without reservation move. At the moment when the picking77 # without reservation move. At the moment when the picking
@@ -80,6 +79,8 @@
80 # the id of the picking's move in this procurement and79 # the id of the picking's move in this procurement and
81 # confirm the procurement80 # confirm the procurement
82 # (see in purchase_order.action_picking_create())81 # (see in purchase_order.action_picking_create())
82 # In there, we'll also take care and confirm all procurements
83 # with product of type service.
8384
84 # set the purchases to direct delivery85 # set the purchases to direct delivery
85 purchase_obj = self.pool.get('purchase.order')86 purchase_obj = self.pool.get('purchase.order')
@@ -133,6 +134,14 @@
133 cr, uid, order, list(lines), picking_id=False, context=context)134 cr, uid, order, list(lines), picking_id=False, context=context)
134 return True135 return True
135136
137 def copy(self, cr, uid, id, default=None, context=None):
138 if not default:
139 default = {}
140 default['invoice_ids'] = False
141 default['requisition_id'] = False
142 return super(sale_order, self).copy(cr, uid, id,
143 default=default, context=context)
144
136145
137class sale_order_line(orm.Model):146class sale_order_line(orm.Model):
138 _inherit = "sale.order.line"147 _inherit = "sale.order.line"
139148
=== modified file 'logistic_requisition/view/logistic_requisition.xml'
--- logistic_requisition/view/logistic_requisition.xml 2013-10-31 09:03:35 +0000
+++ logistic_requisition/view/logistic_requisition.xml 2014-02-06 10:43:21 +0000
@@ -555,7 +555,7 @@
555 </group>555 </group>
556 <group>556 <group>
557 <group string="Purchase Requisition"557 <group string="Purchase Requisition"
558 attrs="{'invisible': [('procurement_method', 'not in', ['procurement', 'fw_agreement'])]}"558 attrs="{'invisible': [('procurement_method', '!=', 'procurement')]}"
559 colspan="4">559 colspan="4">
560 <label for="po_requisition_id"/>560 <label for="po_requisition_id"/>
561 <div>561 <div>
@@ -571,8 +571,8 @@
571 colspan="4"571 colspan="4"
572 attrs="{'invisible': [('procurement_method', '!=', 'wh_dispatch')]}">572 attrs="{'invisible': [('procurement_method', '!=', 'wh_dispatch')]}">
573 <field name="dispatch_location_id"573 <field name="dispatch_location_id"
574 on_change="onchange_dispatch_location_id(dispatch_location_id)" 574 on_change="onchange_dispatch_location_id(dispatch_location_id)"
575 attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}" 575 attrs="{'required': [('procurement_method', '=', 'wh_dispatch')]}"
576 domain="[('usage', '!=', 'view')]"/>576 domain="[('usage', '!=', 'view')]"/>
577 <field name="stock_owner" />577 <field name="stock_owner" />
578 </group>578 </group>
@@ -624,7 +624,7 @@
624 <field name="res_model">logistic.requisition.source</field>624 <field name="res_model">logistic.requisition.source</field>
625 <field name="view_type">form</field>625 <field name="view_type">form</field>
626 <field name="view_mode">tree,form</field>626 <field name="view_mode">tree,form</field>
627 <field name="context">{"search_default_groupby_procurement_method" : True}</field>627 <field name="context">{}</field>
628 <field name="search_view_id" ref="view_logistic_requisition_source_filter"/>628 <field name="search_view_id" ref="view_logistic_requisition_source_filter"/>
629 <field name="help"></field>629 <field name="help"></field>
630 </record>630 </record>
631631
=== modified file 'logistic_requisition/wizard/cost_estimate.py'
--- logistic_requisition/wizard/cost_estimate.py 2013-10-31 15:46:50 +0000
+++ logistic_requisition/wizard/cost_estimate.py 2014-02-06 10:43:21 +0000
@@ -243,9 +243,9 @@
243 'incoterm': requisition.incoterm_id.id,243 'incoterm': requisition.incoterm_id.id,
244 'incoterm_address': requisition.incoterm_address,244 'incoterm_address': requisition.incoterm_address,
245 'requisition_id': requisition.id,245 'requisition_id': requisition.id,
246 'origin': requisition.name,
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,
247 }248 }
248
249 onchange_vals = sale_obj.onchange_partner_id(249 onchange_vals = sale_obj.onchange_partner_id(
250 cr, uid, [], partner_id, context=context).get('value', {})250 cr, uid, [], partner_id, context=context).get('value', {})
251 vals.update(onchange_vals)251 vals.update(onchange_vals)

Subscribers

People subscribed via source and target branches