Merge lp:~julie-w/unifield-server/US-7903 into lp:unifield-server

Proposed by jftempo
Status: Merged
Merged at revision: 6055
Proposed branch: lp:~julie-w/unifield-server/US-7903
Merge into: lp:unifield-server
Diff against target: 1870 lines (+697/-403) (has conflicts)
19 files modified
bin/addons/account_override/account_invoice_sync.py (+44/-33)
bin/addons/account_override/invoice.py (+14/-2)
bin/addons/analytic_distribution/account_commitment.py (+100/-25)
bin/addons/analytic_distribution/account_commitment_view.xml (+45/-9)
bin/addons/analytic_distribution/account_commitment_workflow.xml (+16/-0)
bin/addons/analytic_distribution_supply/invoice.py (+150/-107)
bin/addons/analytic_distribution_supply/stock.py (+16/-32)
bin/addons/base/res/res_log.py (+2/-0)
bin/addons/msf_profile/data/patches.xml (+7/-0)
bin/addons/msf_profile/i18n/fr_MF.po (+94/-15)
bin/addons/msf_profile/msf_profile.py (+13/-0)
bin/addons/purchase/purchase_order.py (+10/-1)
bin/addons/purchase/purchase_order_line.py (+53/-25)
bin/addons/purchase/stock.py (+0/-52)
bin/addons/sale/stock.py (+0/-59)
bin/addons/stock/stock.py (+116/-19)
bin/addons/stock_override/stock.py (+0/-23)
bin/addons/sync_so/purchase.py (+7/-1)
bin/osv/expression.py (+10/-0)
Text conflict in bin/addons/msf_profile/data/patches.xml
Text conflict in bin/addons/msf_profile/i18n/fr_MF.po
Text conflict in bin/addons/msf_profile/msf_profile.py
Text conflict in bin/osv/expression.py
To merge this branch: bzr merge lp:~julie-w/unifield-server/US-7903
Reviewer Review Type Date Requested Status
UniField Reviewer Team Pending
Review via email: mp+406907@code.launchpad.net
To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bin/addons/account_override/account_invoice_sync.py'
--- bin/addons/account_override/account_invoice_sync.py 2021-01-26 11:14:14 +0000
+++ bin/addons/account_override/account_invoice_sync.py 2021-08-11 08:19:49 +0000
@@ -100,10 +100,43 @@
100 line_name = inv_line.get('name', '')100 line_name = inv_line.get('name', '')
101 if not line_name: # required field101 if not line_name: # required field
102 raise osv.except_osv(_('Error'), _("Impossible to retrieve the line description."))102 raise osv.except_osv(_('Error'), _("Impossible to retrieve the line description."))
103 uom_id = False
104 uom_data = inv_line.get('uos_id', {})
105 if uom_data:
106 uom_name = uom_data.get('name', '')
107 uom_ids = product_uom_obj.search(cr, uid, [('name', '=', uom_name)], limit=1, context=context)
108 if not uom_ids:
109 raise osv.except_osv(_('Error'), _("Unit of Measure %s not found.") % uom_name)
110 uom_id = uom_ids[0]
111 quantity = inv_line.get('quantity', 0.0)
112 inv_line_vals = {
113 'invoice_id': inv_id,
114 'name': line_name,
115 'quantity': quantity,
116 'price_unit': inv_line.get('price_unit', 0.0),
117 'discount': inv_line.get('discount', 0.0),
118 'uos_id': uom_id,
119 }
120 line_account_id = False
121 fo_line_dict = inv_line.get('sale_order_line_id') or {}
122 if from_supply and inv_linked_po and fo_line_dict.get('sync_local_id'):
123 # fill in the AD at line level if applicable
124 # search the matching between PO line and invoice line
125 po_line_ids = pol_obj.search(cr, uid, [('order_id', '=', inv_linked_po.id), ('sync_linked_sol', '=', inv_line['sale_order_line_id']['sync_local_id'])], context=context)
126 if po_line_ids:
127 matching_po_line = pol_obj.browse(cr, uid, po_line_ids[0],
128 fields_to_fetch=['analytic_distribution_id', 'cv_line_ids'], context=context)
129 inv_line_vals.update({'order_line_id': matching_po_line.id})
130 if matching_po_line.cv_line_ids:
131 inv_line_vals.update({'cv_line_ids': [(6, 0, [cvl.id for cvl in matching_po_line.cv_line_ids])]})
132 # cv_line_ids only contains one CV line: get its account
133 line_account_id = matching_po_line.cv_line_ids[0].account_id.id
134 po_line_distrib = matching_po_line.analytic_distribution_id
135 self._create_analytic_distrib(cr, uid, inv_line_vals, po_line_distrib, context=context) # update inv_line_vals
103 product_id = False136 product_id = False
104 product_data = inv_line.get('product_id', {})137 product_data = inv_line.get('product_id', {})
105 line_account_id = False138 # for the lines linked to a CV: the CV line account is used (handled above)
106 # for the lines related to a product: use the account of the product / else use the one of the source invoice line139 # for the other lines related to a product: use the account of the product / else use the one of the source invoice line
107 if product_data:140 if product_data:
108 default_code = product_data.get('default_code', '')141 default_code = product_data.get('default_code', '')
109 product_id = so_po_common_obj.get_product_id(cr, uid, product_data, default_code=default_code, context=context) or False142 product_id = so_po_common_obj.get_product_id(cr, uid, product_data, default_code=default_code, context=context) or False
@@ -113,10 +146,11 @@
113 context=context)146 context=context)
114 if not product.active:147 if not product.active:
115 raise osv.except_osv(_('Error'), _("The product %s is inactive.") % product.default_code or '')148 raise osv.except_osv(_('Error'), _("The product %s is inactive.") % product.default_code or '')
116 line_account_id = product.product_tmpl_id.property_account_expense and product.product_tmpl_id.property_account_expense.id149 if not line_account_id:
150 line_account_id = product.product_tmpl_id.property_account_expense and product.product_tmpl_id.property_account_expense.id
117 if not line_account_id:151 if not line_account_id:
118 line_account_id = product.categ_id and product.categ_id.property_account_expense_categ and product.categ_id.property_account_expense_categ.id152 line_account_id = product.categ_id and product.categ_id.property_account_expense_categ and product.categ_id.property_account_expense_categ.id
119 else:153 elif not line_account_id:
120 account_code = inv_line.get('account_id', {}).get('code', '')154 account_code = inv_line.get('account_id', {}).get('code', '')
121 if not account_code:155 if not account_code:
122 raise osv.except_osv(_('Error'), _("Impossible to retrieve the account code at line level."))156 raise osv.except_osv(_('Error'), _("Impossible to retrieve the account code at line level."))
@@ -131,34 +165,9 @@
131 if inv_posting_date < line_account.activation_date or \165 if inv_posting_date < line_account.activation_date or \
132 (line_account.inactivation_date and inv_posting_date >= line_account.inactivation_date):166 (line_account.inactivation_date and inv_posting_date >= line_account.inactivation_date):
133 raise osv.except_osv(_('Error'), _('The account "%s - %s" is inactive.') % (line_account.code, line_account.name))167 raise osv.except_osv(_('Error'), _('The account "%s - %s" is inactive.') % (line_account.code, line_account.name))
134 uom_id = False168 inv_line_vals.update({'account_id': line_account_id,
135 uom_data = inv_line.get('uos_id', {})169 'product_id': product_id,
136 if uom_data:170 })
137 uom_name = uom_data.get('name', '')
138 uom_ids = product_uom_obj.search(cr, uid, [('name', '=', uom_name)], limit=1, context=context)
139 if not uom_ids:
140 raise osv.except_osv(_('Error'), _("Unit of Measure %s not found.") % uom_name)
141 uom_id = uom_ids[0]
142 quantity = inv_line.get('quantity', 0.0)
143 inv_line_vals = {
144 'invoice_id': inv_id,
145 'account_id': line_account_id,
146 'name': line_name,
147 'quantity': quantity,
148 'price_unit': inv_line.get('price_unit', 0.0),
149 'discount': inv_line.get('discount', 0.0),
150 'product_id': product_id,
151 'uos_id': uom_id,
152 }
153 fo_line_dict = inv_line.get('sale_order_line_id') or {}
154 if from_supply and inv_linked_po and fo_line_dict.get('sync_local_id'):
155 # fill in the AD at line level if applicable
156 # search the matching between PO line and invoice line
157 po_line_ids = pol_obj.search(cr, uid, [('order_id', '=', inv_linked_po.id), ('sync_linked_sol', '=', inv_line['sale_order_line_id']['sync_local_id'])], context=context)
158 if po_line_ids:
159 matching_po_line = pol_obj.browse(cr, uid, po_line_ids[0], fields_to_fetch=['analytic_distribution_id'], context=context)
160 po_line_distrib = matching_po_line.analytic_distribution_id
161 self._create_analytic_distrib(cr, uid, inv_line_vals, po_line_distrib, context=context) # update inv_line_vals
162 inv_line_obj.create(cr, uid, inv_line_vals, context=context)171 inv_line_obj.create(cr, uid, inv_line_vals, context=context)
163172
164 def create_invoice_from_sync(self, cr, uid, source, invoice_data, context=None):173 def create_invoice_from_sync(self, cr, uid, source, invoice_data, context=None):
@@ -270,7 +279,9 @@
270 if po_ids:279 if po_ids:
271 po_id = po_ids[0]280 po_id = po_ids[0]
272 if po_id:281 if po_id:
273 vals.update({'main_purchase_id': po_id})282 vals.update({'main_purchase_id': po_id,
283 'purchase_ids': [(6, 0, [po_id])],
284 })
274 po_fields = ['picking_ids', 'analytic_distribution_id', 'order_line', 'name']285 po_fields = ['picking_ids', 'analytic_distribution_id', 'order_line', 'name']
275 po = po_obj.browse(cr, uid, po_id, fields_to_fetch=po_fields, context=context)286 po = po_obj.browse(cr, uid, po_id, fields_to_fetch=po_fields, context=context)
276 po_number = po.name287 po_number = po.name
277288
=== modified file 'bin/addons/account_override/invoice.py'
--- bin/addons/account_override/invoice.py 2021-04-29 16:00:54 +0000
+++ bin/addons/account_override/invoice.py 2021-08-11 08:19:49 +0000
@@ -1423,6 +1423,8 @@
1423 vals = by_account_vals[l.account_id.id]1423 vals = by_account_vals[l.account_id.id]
1424 if l.order_line_id:1424 if l.order_line_id:
1425 vals.setdefault('purchase_order_line_ids', []).append(l.order_line_id.id)1425 vals.setdefault('purchase_order_line_ids', []).append(l.order_line_id.id)
1426 if l.cv_line_ids:
1427 vals.setdefault('cv_line_ids', []).extend([cvl.id for cvl in l.cv_line_ids])
1426 else:1428 else:
1427 # new account to merge1429 # new account to merge
1428 vals = vals_template.copy()1430 vals = vals_template.copy()
@@ -1430,9 +1432,12 @@
1430 '_index_': index,1432 '_index_': index,
1431 'account_id': l.account_id.id,1433 'account_id': l.account_id.id,
1432 'purchase_order_line_ids': [],1434 'purchase_order_line_ids': [],
1435 'cv_line_ids': [],
1433 })1436 })
1434 if l.order_line_id:1437 if l.order_line_id:
1435 vals['purchase_order_line_ids'].append(l.order_line_id.id)1438 vals['purchase_order_line_ids'].append(l.order_line_id.id)
1439 if l.cv_line_ids:
1440 vals['cv_line_ids'].extend([cvl.id for cvl in l.cv_line_ids])
1436 index += 11441 index += 1
14371442
1438 '''1443 '''
@@ -1515,6 +1520,8 @@
15151520
1516 vals['purchase_order_line_ids'] = vals['purchase_order_line_ids'] and [(6, 0, vals['purchase_order_line_ids'])] or False1521 vals['purchase_order_line_ids'] = vals['purchase_order_line_ids'] and [(6, 0, vals['purchase_order_line_ids'])] or False
15171522
1523 vals['cv_line_ids'] = vals['cv_line_ids'] and [(6, 0, vals['cv_line_ids'])] or False
1524
1518 # create merge line1525 # create merge line
1519 vals.update({'merged_line': True})1526 vals.update({'merged_line': True})
1520 if not self.pool.get('account.invoice.line').create(cr, uid,1527 if not self.pool.get('account.invoice.line').create(cr, uid,
@@ -1797,6 +1804,10 @@
1797 ('out_refund', 'Customer Refund'),1804 ('out_refund', 'Customer Refund'),
1798 ('in_refund', 'Supplier Refund')]),1805 ('in_refund', 'Supplier Refund')]),
1799 'merged_line': fields.boolean(string='Merged Line', help='Line generated by the merging of other lines', readonly=True),1806 'merged_line': fields.boolean(string='Merged Line', help='Line generated by the merging of other lines', readonly=True),
1807 # - a CV line can be linked to several invoice lines ==> e.g. several partial deliveries, split of invoice lines
1808 # - an invoice line can be linked to several CV lines => e.g. merge invoice lines by account
1809 'cv_line_ids': fields.many2many('account.commitment.line', 'inv_line_cv_line_rel', 'inv_line_id', 'cv_line_id',
1810 string='Commitment Voucher Lines'),
1800 }1811 }
18011812
1802 _defaults = {1813 _defaults = {
@@ -1943,7 +1954,7 @@
1943 """1954 """
1944 Copy an invoice line without its move lines,1955 Copy an invoice line without its move lines,
1945 without the link to a reversed invoice line,1956 without the link to a reversed invoice line,
1946 and without link to PO/FO lines when the duplication is manual1957 and without link to PO/FO/CV lines when the duplication is manual
1947 Reset the merged_line tag.1958 Reset the merged_line tag.
1948 """1959 """
1949 if context is None:1960 if context is None:
@@ -1955,13 +1966,14 @@
1955 'merged_line': False,1966 'merged_line': False,
1956 })1967 })
1957 # Manual duplication should generate a "manual document not created through the supply workflow"1968 # Manual duplication should generate a "manual document not created through the supply workflow"
1958 # so we don't keep the link to PO/FO at line level1969 # so we don't keep the link to PO/FO/CV at line level
1959 if context.get('from_button') and not context.get('from_split'):1970 if context.get('from_button') and not context.get('from_split'):
1960 default.update({1971 default.update({
1961 'order_line_id': False,1972 'order_line_id': False,
1962 'sale_order_line_id': False,1973 'sale_order_line_id': False,
1963 'sale_order_lines': False,1974 'sale_order_lines': False,
1964 'purchase_order_line_ids': [],1975 'purchase_order_line_ids': [],
1976 'cv_line_ids': [(6, 0, [])],
1965 })1977 })
1966 return super(account_invoice_line, self).copy_data(cr, uid, inv_id, default, context)1978 return super(account_invoice_line, self).copy_data(cr, uid, inv_id, default, context)
19671979
19681980
=== modified file 'bin/addons/analytic_distribution/account_commitment.py'
--- bin/addons/analytic_distribution/account_commitment.py 2021-05-11 18:16:37 +0000
+++ bin/addons/analytic_distribution/account_commitment.py 2021-08-11 08:19:49 +0000
@@ -67,6 +67,38 @@
67 res.append(cvl.commit_id.id)67 res.append(cvl.commit_id.id)
68 return res68 return res
6969
70 def get_cv_type(self, cr, uid, context=None):
71 """
72 Returns the list of possible types for the Commitment Vouchers
73 """
74 return [('manual', 'Manual'),
75 ('external', 'Automatic - External supplier'),
76 ('esc', 'Manual - ESC supplier'),
77 ('intermission', 'Automatic - Intermission'),
78 ('intersection', 'Automatic - Intersection'),
79 ]
80
81 def get_current_cv_version(self, cr, uid, context=None):
82 """
83 Version 2 since US-7449
84 """
85 return 2
86
87 def _display_super_done_button(self, cr, uid, ids, name, arg, context=None):
88 """
89 For now the "Super" Done button, which allows to always set a CV to Done whatever its state and origin,
90 is visible only by the Admin user. It is displayed only when the standard Done button isn't usable.
91 """
92 if context is None:
93 context = {}
94 if isinstance(ids, (int, long)):
95 ids = [ids]
96 res = {}
97 for cv in self.read(cr, uid, ids, ['state', 'type'], context=context):
98 other_done_button_usable = cv['state'] == 'open' and cv['type'] not in ('external', 'intermission', 'intersection')
99 res[cv['id']] = not other_done_button_usable and uid == 1 and cv['state'] != 'done'
100 return res
101
70 _columns = {102 _columns = {
71 'journal_id': fields.many2one('account.analytic.journal', string="Journal", readonly=True, required=True),103 'journal_id': fields.many2one('account.analytic.journal', string="Journal", readonly=True, required=True),
72 'name': fields.char(string="Number", size=64, readonly=True, required=True),104 'name': fields.char(string="Number", size=64, readonly=True, required=True),
@@ -81,16 +113,22 @@
81 'account.commitment.line': (_get_cv, ['amount'],10),113 'account.commitment.line': (_get_cv, ['amount'],10),
82 }),114 }),
83 'analytic_distribution_id': fields.many2one('analytic.distribution', string="Analytic distribution"),115 'analytic_distribution_id': fields.many2one('analytic.distribution', string="Analytic distribution"),
84 'type': fields.selection([('manual', 'Manual'), ('external', 'Automatic - External supplier'), ('esc', 'Manual - ESC supplier')], string="Type", readonly=True),116 'type': fields.selection(get_cv_type, string="Type", readonly=True),
85 'notes': fields.text(string="Comment"),117 'notes': fields.text(string="Comment"),
86 'purchase_id': fields.many2one('purchase.order', string="Source document", readonly=True),118 'purchase_id': fields.many2one('purchase.order', string="Source document", readonly=True),
87 'description': fields.char(string="Description", size=256),119 'description': fields.char(string="Description", size=256),
120 'version': fields.integer('Version', required=True,
121 help="Technical field to distinguish old CVs from new ones which have a different behavior."),
122 'display_super_done_button': fields.function(_display_super_done_button, method=True, type='boolean',
123 store=False, invisible=True,
124 string='Display the button allowing to always set a CV to Done'),
88 }125 }
89126
90 _defaults = {127 _defaults = {
91 'state': lambda *a: 'draft',128 'state': lambda *a: 'draft',
92 'date': lambda *a: strftime('%Y-%m-%d'),129 'date': lambda *a: strftime('%Y-%m-%d'),
93 'type': lambda *a: 'manual',130 'type': lambda *a: 'manual',
131 'version': get_current_cv_version,
94 'journal_id': lambda s, cr, uid, c: s.pool.get('account.analytic.journal').search(cr, uid, [('type', '=', 'engagement'),132 'journal_id': lambda s, cr, uid, c: s.pool.get('account.analytic.journal').search(cr, uid, [('type', '=', 'engagement'),
95 ('instance_id', '=', s.pool.get('res.users').browse(cr, uid, uid, c).company_id.instance_id.id)], limit=1, context=c)[0]133 ('instance_id', '=', s.pool.get('res.users').browse(cr, uid, uid, c).company_id.instance_id.id)], limit=1, context=c)[0]
96 }134 }
@@ -181,29 +219,30 @@
181 fctal_currency = user_obj.browse(cr, uid, uid, fields_to_fetch=['company_id'], context=context).company_id.currency_id.id219 fctal_currency = user_obj.browse(cr, uid, uid, fields_to_fetch=['company_id'], context=context).company_id.currency_id.id
182 for cl in c.line_ids:220 for cl in c.line_ids:
183 # Verify that date is compatible with all analytic account from distribution221 # Verify that date is compatible with all analytic account from distribution
222 distrib = False
184 if cl.analytic_distribution_id:223 if cl.analytic_distribution_id:
185 distrib = cl.analytic_distribution_id224 distrib = cl.analytic_distribution_id
186 elif cl.commit_id and cl.commit_id.analytic_distribution_id:225 elif cl.commit_id and cl.commit_id.analytic_distribution_id:
187 distrib = cl.commit_id.analytic_distribution_id226 distrib = cl.commit_id.analytic_distribution_id
188 else:227 if distrib:
189 raise osv.except_osv(_('Warning'), _('No analytic distribution found for %s %s') % (cl.account_id.code, cl.initial_amount))228 for distrib_lines in [distrib.cost_center_lines, distrib.funding_pool_lines,
190 for distrib_lines in [distrib.cost_center_lines, distrib.funding_pool_lines, distrib.free_1_lines, distrib.free_2_lines]:229 distrib.free_1_lines, distrib.free_2_lines]:
191 for distrib_line in distrib_lines:230 for distrib_line in distrib_lines:
192 if distrib_line.analytic_id and \231 if distrib_line.analytic_id and \
193 (distrib_line.analytic_id.date_start and date < distrib_line.analytic_id.date_start or232 (distrib_line.analytic_id.date_start and date < distrib_line.analytic_id.date_start or
194 distrib_line.analytic_id.date and date >= distrib_line.analytic_id.date):233 distrib_line.analytic_id.date and date >= distrib_line.analytic_id.date):
195 raise osv.except_osv(_('Error'), _('The analytic account %s is not active for given date.') %234 raise osv.except_osv(_('Error'), _('The analytic account %s is not active for given date.') %
196 (distrib_line.analytic_id.name,))235 (distrib_line.analytic_id.name,))
197 dest_cc_tuples = set() # check each Dest/CC combination only once236 dest_cc_tuples = set() # check each Dest/CC combination only once
198 for distrib_cc_l in distrib.cost_center_lines:237 for distrib_cc_l in distrib.cost_center_lines:
199 if distrib_cc_l.analytic_id: # non mandatory field238 if distrib_cc_l.analytic_id: # non mandatory field
200 dest_cc_tuples.add((distrib_cc_l.destination_id, distrib_cc_l.analytic_id))239 dest_cc_tuples.add((distrib_cc_l.destination_id, distrib_cc_l.analytic_id))
201 for distrib_fp_l in distrib.funding_pool_lines:240 for distrib_fp_l in distrib.funding_pool_lines:
202 dest_cc_tuples.add((distrib_fp_l.destination_id, distrib_fp_l.cost_center_id))241 dest_cc_tuples.add((distrib_fp_l.destination_id, distrib_fp_l.cost_center_id))
203 for dest, cc in dest_cc_tuples:242 for dest, cc in dest_cc_tuples:
204 if dest_cc_link_obj.is_inactive_dcl(cr, uid, dest.id, cc.id, date, context=context):243 if dest_cc_link_obj.is_inactive_dcl(cr, uid, dest.id, cc.id, date, context=context):
205 raise osv.except_osv(_('Error'), _("The combination \"%s - %s\" is not active at this date: %s") %244 raise osv.except_osv(_('Error'), _("The combination \"%s - %s\" is not active at this date: %s") %
206 (dest.code or '', cc.code or '', date))245 (dest.code or '', cc.code or '', date))
207 # update the dates and fctal amounts of the related analytic lines246 # update the dates and fctal amounts of the related analytic lines
208 context.update({'currency_date': date}) # same date used for doc, posting and source date of all lines247 context.update({'currency_date': date}) # same date used for doc, posting and source date of all lines
209 for aal in cl.analytic_lines:248 for aal in cl.analytic_lines:
@@ -233,6 +272,7 @@
233 default.update({272 default.update({
234 'name': self.pool.get('ir.sequence').get(cr, uid, 'account.commitment'),273 'name': self.pool.get('ir.sequence').get(cr, uid, 'account.commitment'),
235 'state': 'draft',274 'state': 'draft',
275 'version': self.get_current_cv_version(cr, uid, context=context),
236 })276 })
237 # Default method277 # Default method
238 res = super(account_commitment, self).copy(cr, uid, c_id, default, context)278 res = super(account_commitment, self).copy(cr, uid, c_id, default, context)
@@ -312,6 +352,12 @@
312 'context': context,352 'context': context,
313 }353 }
314354
355 def button_analytic_distribution_2(self, cr, uid, ids, context=None):
356 """
357 This is just an alias for button_analytic_distribution (used to have different names and attrs on both buttons)
358 """
359 return self.button_analytic_distribution(cr, uid, ids, context=context)
360
315 def button_reset_distribution(self, cr, uid, ids, context=None):361 def button_reset_distribution(self, cr, uid, ids, context=None):
316 """362 """
317 Reset analytic distribution on all commitment lines.363 Reset analytic distribution on all commitment lines.
@@ -444,10 +490,11 @@
444 # Search analytic lines that have commitment line ids490 # Search analytic lines that have commitment line ids
445 search_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', 'in', [x.id for x in c.line_ids])], context=context)491 search_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', 'in', [x.id for x in c.line_ids])], context=context)
446 # Delete them492 # Delete them
447 res = self.pool.get('account.analytic.line').unlink(cr, uid, search_ids, context=context)493 if search_ids:
494 res = self.pool.get('account.analytic.line').unlink(cr, uid, search_ids, context=context)
495 if not res:
496 raise osv.except_osv(_('Error'), _('An error occurred on engagement lines deletion.'))
448 # And finally update commitment voucher state and lines amount497 # And finally update commitment voucher state and lines amount
449 if not res:
450 raise osv.except_osv(_('Error'), _('An error occurred on engagement lines deletion.'))
451 self.pool.get('account.commitment.line').write(cr, uid, [x.id for x in c.line_ids], {'amount': 0}, context=context)498 self.pool.get('account.commitment.line').write(cr, uid, [x.id for x in c.line_ids], {'amount': 0}, context=context)
452 self.write(cr, uid, [c.id], {'state':'done'}, context=context)499 self.write(cr, uid, [c.id], {'state':'done'}, context=context)
453 return True500 return True
@@ -457,7 +504,7 @@
457class account_commitment_line(osv.osv):504class account_commitment_line(osv.osv):
458 _name = 'account.commitment.line'505 _name = 'account.commitment.line'
459 _description = "Account Commitment Voucher Line"506 _description = "Account Commitment Voucher Line"
460 _order = "id desc"507 _order = "po_line_id, id desc"
461 _rec_name = 'account_id'508 _rec_name = 'account_id'
462 _trace = True509 _trace = True
463510
@@ -497,6 +544,12 @@
497 res[co.id] = False544 res[co.id] = False
498 return res545 return res
499546
547 def get_cv_type(self, cr, uid, context=None):
548 """
549 Gets the possible CV types
550 """
551 return self.pool.get('account.commitment').get_cv_type(cr, uid, context)
552
500 _columns = {553 _columns = {
501 'account_id': fields.many2one('account.account', string="Account", required=True),554 'account_id': fields.many2one('account.account', string="Account", required=True),
502 'amount': fields.float(string="Amount left", digits_compute=dp.get_precision('Account'), required=False),555 'amount': fields.float(string="Amount left", digits_compute=dp.get_precision('Account'), required=False),
@@ -504,6 +557,8 @@
504 'commit_id': fields.many2one('account.commitment', string="Commitment Voucher", on_delete="cascade"),557 'commit_id': fields.many2one('account.commitment', string="Commitment Voucher", on_delete="cascade"),
505 'commit_number': fields.related('commit_id', 'name', type='char', size=64,558 'commit_number': fields.related('commit_id', 'name', type='char', size=64,
506 readonly=True, store=False, string="Commitment Voucher Number"),559 readonly=True, store=False, string="Commitment Voucher Number"),
560 'commit_type': fields.related('commit_id', 'type', string="Commitment Voucher Type", type='selection', readonly=True,
561 store=False, invisible=True, selection=get_cv_type, write_relate=False),
507 'analytic_distribution_id': fields.many2one('analytic.distribution', string="Analytic distribution"),562 'analytic_distribution_id': fields.many2one('analytic.distribution', string="Analytic distribution"),
508 'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',563 'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',
509 selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],564 selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],
@@ -513,8 +568,15 @@
513 'analytic_lines': fields.one2many('account.analytic.line', 'commitment_line_id', string="Analytic Lines"),568 'analytic_lines': fields.one2many('account.analytic.line', 'commitment_line_id', string="Analytic Lines"),
514 'first': fields.boolean(string="Is not created?", help="Useful for onchange method for views. Should be False after line creation.",569 'first': fields.boolean(string="Is not created?", help="Useful for onchange method for views. Should be False after line creation.",
515 readonly=True),570 readonly=True),
571 # for CV in version 1
516 'purchase_order_line_ids': fields.many2many('purchase.order.line', 'purchase_line_commitment_rel', 'commitment_id', 'purchase_id',572 'purchase_order_line_ids': fields.many2many('purchase.order.line', 'purchase_line_commitment_rel', 'commitment_id', 'purchase_id',
517 string="Purchase Order Lines", readonly=True),573 string="Purchase Order Lines (deprecated)", readonly=True),
574 # for CV starting from version 2
575 'po_line_id': fields.many2one('purchase.order.line', "PO Line"),
576 'po_line_product_id': fields.related('po_line_id', 'product_id', type='many2one', relation='product.product',
577 string="Product", readonly=True, store=True, write_relate=False),
578 'po_line_number': fields.related('po_line_id', 'line_number', type='integer_null', string="PO Line", readonly=True,
579 store=True, write_relate=False, _fnct_migrate=lambda *a: True),
518 }580 }
519581
520 _defaults = {582 _defaults = {
@@ -656,6 +718,19 @@
656 self.update_analytic_lines(cr, uid, [line.id], vals.get('amount'), account_id, context=context)718 self.update_analytic_lines(cr, uid, [line.id], vals.get('amount'), account_id, context=context)
657 return super(account_commitment_line, self).write(cr, uid, ids, vals, context={})719 return super(account_commitment_line, self).write(cr, uid, ids, vals, context={})
658720
721 def copy_data(self, cr, uid, cv_line_id, default=None, context=None):
722 """
723 Duplicates a CV line: resets the link to PO line
724 """
725 if context is None:
726 context = {}
727 if default is None:
728 default = {}
729 default.update({
730 'po_line_id': False,
731 })
732 return super(account_commitment_line, self).copy_data(cr, uid, cv_line_id, default, context)
733
659 def button_analytic_distribution(self, cr, uid, ids, context=None):734 def button_analytic_distribution(self, cr, uid, ids, context=None):
660 """735 """
661 Launch analytic distribution wizard on a commitment voucher line736 Launch analytic distribution wizard on a commitment voucher line
662737
=== modified file 'bin/addons/analytic_distribution/account_commitment_view.xml'
--- bin/addons/analytic_distribution/account_commitment_view.xml 2020-09-23 09:29:11 +0000
+++ bin/addons/analytic_distribution/account_commitment_view.xml 2021-08-11 08:19:49 +0000
@@ -28,20 +28,36 @@
28 <button name="button_reset_distribution" string="Reset AD at line level" type="object" icon="gtk-undelete" colspan="4" states="draft"/>28 <button name="button_reset_distribution" string="Reset AD at line level" type="object" icon="gtk-undelete" colspan="4" states="draft"/>
29 </group>29 </group>
30 <group colspan="8" col="8" attrs="{'invisible': [('analytic_distribution_id', '!=', False)]}">30 <group colspan="8" col="8" attrs="{'invisible': [('analytic_distribution_id', '!=', False)]}">
31 <button name="button_analytic_distribution" string="Analytical Distribution" type="object" icon="terp-emblem-important" context="context" colspan="4" attrs="{'invisible': [('analytic_distribution_id', '!=', False)]}"/>31 <button name="button_analytic_distribution_2" string="Analytical Distribution"
32 type="object" icon="terp-emblem-important" context="context" colspan="4"
33 attrs="{'readonly': [('state', '=', 'open'), ('type', '=', 'manual')]}"/>
32 <button name="button_reset_distribution" string="Reset AD at line level" type="object" icon="gtk-undelete" colspan="4" states="draft"/>34 <button name="button_reset_distribution" string="Reset AD at line level" type="object" icon="gtk-undelete" colspan="4" states="draft"/>
33 </group>35 </group>
34 <field name="analytic_distribution_id" invisible="1"/>36 <field name="analytic_distribution_id" invisible="1"/>
35 <notebook colspan="4">37 <notebook colspan="4">
36 <page string="Commitment voucher lines">38 <page string="Commitment voucher lines">
37 <field name="line_ids" nolabel="1" colspan="4" attrs="{'readonly': ['|', ('state', '=', 'done'), ('type', '=', 'external')]}"/>39 <field name="line_ids" nolabel="1" colspan="4"
40 attrs="{'readonly': ['|', ('state', '=', 'done'),
41 '&amp;',
42 ('type', 'in', ['external', 'intermission', 'intersection']),
43 ('state', '!=', 'draft')]}"
44 />
38 </page>45 </page>
39 </notebook>46 </notebook>
40 <field name="notes" colspan="4"/>47 <field name="notes" colspan="4"/>
41 <group col="6" colspan="4">48 <group col="6" colspan="4">
42 <button name="button_compute" string="Compute total" icon="gtk-execute" colspan="2"/>49 <button name="button_compute" string="Compute total" icon="gtk-execute" colspan="2"/>
43 <button name="commitment_open" string="Validate" icon="terp-camera_test" states="draft" colspan="2"/>50 <button name="commitment_open" string="Validate" icon="terp-camera_test" states="draft" colspan="2"/>
44 <button name="commitment_validate" string="Done" icon="terp-gtk-go-back-rtl" states="open" colspan="2" attrs="{'readonly': [('type', '=', 'external')]}"/>51 <field name="display_super_done_button"/>
52 <button name="commitment_validate" string="Done" icon="terp-gtk-go-back-rtl" colspan="2"
53 attrs="{'invisible': ['|', ('state', '!=', 'open'), ('display_super_done_button', '=', True)],
54 'readonly': [('type', 'in', ['external', 'intermission', 'intersection'])]}"
55 />
56 <!-- button which allows to always set a CV to Done (whatever its state and origin) -->
57 <button name="commitment_always_validate" string="Done (for Administrator only)"
58 icon="terp-gtk-go-back-rtl" colspan="6"
59 confirm='You are about to set this Commitment Voucher to the state "Done". Do you want to proceed?'
60 attrs="{'invisible': [('display_super_done_button', '=', False)]}"/>
45 </group>61 </group>
46 <field name="state"/>62 <field name="state"/>
47 <field name="total"/>63 <field name="total"/>
@@ -73,14 +89,32 @@
73 <field name="model">account.commitment.line</field>89 <field name="model">account.commitment.line</field>
74 <field name="type">tree</field>90 <field name="type">tree</field>
75 <field name="arch" type="xml">91 <field name="arch" type="xml">
76 <tree string="Commitment Voucher Lines" editable="top" colors="red:analytic_distribution_state == 'invalid'">92 <!-- display the "New" button or not depending on the state and type of the CV itself -->
93 <tree string="Commitment Voucher Lines" editable="top" colors="red:analytic_distribution_state == 'invalid'"
94 button_attrs="{'invisible': ['|', ('state', '=', 'done'), ('type', 'in', ['external', 'intermission', 'intersection'])]}"
95 hide_delete_button="1"
96 >
97 <field name="commit_type"/>
98 <field name="po_line_product_id"/>
99 <field name="po_line_number"/>
77 <field name="account_id" domain="[('restricted_area', '=', 'commitment_lines')]"/>100 <field name="account_id" domain="[('restricted_area', '=', 'commitment_lines')]"/>
78 <button name="button_analytic_distribution" string="Analytical Distribution" type="object" icon="terp-stock_symbol-selection" context="context"/>101 <button name="button_analytic_distribution" string="Analytical Distribution" type="object"
79 <field name="analytic_distribution_state"/>102 icon="terp-stock_symbol-selection" context="context"
80 <field name="have_analytic_distribution_from_header"/>103 />
104 <field name="analytic_distribution_state" readonly="1"/>
105 <field name="have_analytic_distribution_from_header" readonly="1"/>
81 <field name="first" invisible="1"/>106 <field name="first" invisible="1"/>
82 <field name="initial_amount" on_change="onchange_initial_amount(first, initial_amount)"/>107 <field name="initial_amount" on_change="onchange_initial_amount(first, initial_amount)"
83 <field name="amount" attrs="{'readonly': [('first', '=', True)]}"/>108 attrs="{'readonly': [('commit_type', 'in', ['external', 'intermission', 'intersection'])]}"
109 />
110 <field name="amount"
111 attrs="{'readonly': ['|', ('first', '=', True), ('commit_type', 'in', ['external', 'intermission', 'intersection'])]}"
112 />
113 <!-- display the "Delete" button or not depending on the state and type of the CV itself -->
114 <button name="unlink" string="Delete" icon="gtk-del" type="object"
115 attrs="{'invisible': ['|', ('state', '=', 'done'), ('type', 'in', ['external', 'intermission', 'intersection'])]}"
116 confirm="Do you really want to delete this line?"
117 />
84 </tree>118 </tree>
85 </field>119 </field>
86 </record>120 </record>
@@ -95,6 +129,8 @@
95 <group col='6' colspan='4'>129 <group col='6' colspan='4'>
96 <filter icon="terp-tools" string="Manual" domain="[('type','=','manual')]" help="Manual Commitment Voucher"/>130 <filter icon="terp-tools" string="Manual" domain="[('type','=','manual')]" help="Manual Commitment Voucher"/>
97 <filter icon="gtk-quit" string="External" domain="[('type','=','external')]" help="External Commitment Voucher"/>131 <filter icon="gtk-quit" string="External" domain="[('type','=','external')]" help="External Commitment Voucher"/>
132 <filter icon="gtk-refresh" string="Intersection" domain="[('type', '=', 'intersection')]" help="Intersection Commitment Voucher"/>
133 <filter icon="gtk-ok" string="Intermission" domain="[('type', '=', 'intermission')]" help="Intermission Commitment Voucher"/>
98 <filter icon="terp-partner" string="ESC" domain="[('type','=','esc')]" help="ESC Commitment Voucher"/>134 <filter icon="terp-partner" string="ESC" domain="[('type','=','esc')]" help="ESC Commitment Voucher"/>
99 <separator orientation="vertical"/>135 <separator orientation="vertical"/>
100 <filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Commitment Voucher in Draft state" name="draft"/>136 <filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Commitment Voucher in Draft state" name="draft"/>
101137
=== modified file 'bin/addons/analytic_distribution/account_commitment_workflow.xml'
--- bin/addons/analytic_distribution/account_commitment_workflow.xml 2011-11-30 14:56:38 +0000
+++ bin/addons/analytic_distribution/account_commitment_workflow.xml 2021-08-11 08:19:49 +0000
@@ -34,14 +34,30 @@
34 <record id="commit_t1" model="workflow.transition">34 <record id="commit_t1" model="workflow.transition">
35 <field name="act_from" ref="act_draft"/>35 <field name="act_from" ref="act_draft"/>
36 <field name="act_to" ref="act_open"/>36 <field name="act_to" ref="act_open"/>
37 <field name="sequence">10</field>
37 <field name="signal">commitment_open</field>38 <field name="signal">commitment_open</field>
38 </record>39 </record>
3940
40 <record id="commit_t2" model="workflow.transition">41 <record id="commit_t2" model="workflow.transition">
41 <field name="act_from" ref="act_open"/>42 <field name="act_from" ref="act_open"/>
42 <field name="act_to" ref="act_done"/>43 <field name="act_to" ref="act_done"/>
44 <field name="sequence">10</field>
43 <field name="signal">commitment_validate</field>45 <field name="signal">commitment_validate</field>
44 </record>46 </record>
4547
48 <record id="commit_t3" model="workflow.transition">
49 <field name="act_from" ref="act_open"/>
50 <field name="act_to" ref="act_done"/>
51 <field name="sequence">15</field> <!-- (sequence, act_from) must be unique -->
52 <field name="signal">commitment_always_validate</field>
53 </record>
54
55 <record id="commit_t4" model="workflow.transition">
56 <field name="act_from" ref="act_draft"/>
57 <field name="act_to" ref="act_done"/>
58 <field name="sequence">15</field> <!-- (sequence, act_from) must be unique -->
59 <field name="signal">commitment_always_validate</field>
60 </record>
61
46 </data>62 </data>
47</openerp>63</openerp>
4864
=== modified file 'bin/addons/analytic_distribution_supply/invoice.py'
--- bin/addons/analytic_distribution_supply/invoice.py 2021-01-26 13:48:04 +0000
+++ bin/addons/analytic_distribution_supply/invoice.py 2021-08-11 08:19:49 +0000
@@ -26,6 +26,7 @@
26from osv import fields26from osv import fields
27from tools.translate import _27from tools.translate import _
28from base import currency_date28from base import currency_date
29import netsvc
2930
3031
31class account_invoice_line(osv.osv):32class account_invoice_line(osv.osv):
@@ -93,20 +94,27 @@
93 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'analytic_distribution_id': new_distrib_id,})94 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'analytic_distribution_id': new_distrib_id,})
94 # Then set distribution on invoice line regarding purchase order line distribution95 # Then set distribution on invoice line regarding purchase order line distribution
95 for invl in inv.invoice_line:96 for invl in inv.invoice_line:
96 if invl.order_line_id:97 line_distrib_id = False
98 if invl.cv_line_ids:
99 # CV STARTING FROM VERSION 2
100 # the first CV line found is used since there can be only one at this step (merging lines by account could
101 # generate an invoice line linked to several CV lines but this action can only be done later in the process)
102 line_distrib_id = invl.cv_line_ids[0].analytic_distribution_id and invl.cv_line_ids[0].analytic_distribution_id.id or False
103 elif invl.order_line_id:
97 # Fetch PO line analytic distribution or nothing (that implies it take those from PO)104 # Fetch PO line analytic distribution or nothing (that implies it take those from PO)
98 distrib_id = invl.order_line_id.analytic_distribution_id and invl.order_line_id.analytic_distribution_id.id or False105 line_distrib_id = invl.order_line_id.analytic_distribution_id and invl.order_line_id.analytic_distribution_id.id or False
99 # Attempt to fetch commitment line analytic distribution or commitment voucher analytic distribution or default distrib_id106 # Attempt to fetch commitment line analytic distribution or commitment voucher analytic distribution or default distrib_id
107 # CV IN VERSION 1
100 if invl.order_line_id.commitment_line_ids:108 if invl.order_line_id.commitment_line_ids:
101 distrib_id = invl.order_line_id.commitment_line_ids[0].analytic_distribution_id \109 line_distrib_id = invl.order_line_id.commitment_line_ids[0].analytic_distribution_id \
102 and invl.order_line_id.commitment_line_ids[0].analytic_distribution_id.id or distrib_id110 and invl.order_line_id.commitment_line_ids[0].analytic_distribution_id.id or line_distrib_id
103 if distrib_id:111 if line_distrib_id:
104 new_invl_distrib_id = ana_obj.copy(cr, uid, distrib_id, {})112 new_invl_distrib_id = ana_obj.copy(cr, uid, line_distrib_id, {})
105 if not new_invl_distrib_id:113 if not new_invl_distrib_id:
106 raise osv.except_osv(_('Error'), _('An error occurred for analytic distribution copy for invoice.'))114 raise osv.except_osv(_('Error'), _('An error occurred for analytic distribution copy for invoice.'))
107 # create default funding pool lines115 # create default funding pool lines
108 ana_obj.create_funding_pool_lines(cr, uid, [new_invl_distrib_id], invl.account_id.id)116 ana_obj.create_funding_pool_lines(cr, uid, [new_invl_distrib_id], invl.account_id.id)
109 invl_obj.write(cr, uid, [invl.id], {'analytic_distribution_id': new_invl_distrib_id})117 invl_obj.write(cr, uid, [invl.id], {'analytic_distribution_id': new_invl_distrib_id})
110 # Fetch SO line analytic distribution118 # Fetch SO line analytic distribution
111 # sol AD copy moved into _invoice_line_hook119 # sol AD copy moved into _invoice_line_hook
112 return True120 return True
@@ -124,7 +132,8 @@
124132
125 # Browse invoices133 # Browse invoices
126 for inv in self.browse(cr, uid, ids, context=context):134 for inv in self.browse(cr, uid, ids, context=context):
127 grouped_invl = {}135 grouped_invl_by_acc = {}
136 grouped_invl_by_cvl = {}
128 co_ids = self.pool.get('account.commitment').search(cr, uid, [('purchase_id', 'in', [x.id for x in inv.purchase_ids]), ('state', 'in', ['open', 'draft'])], order='date desc', context=context)137 co_ids = self.pool.get('account.commitment').search(cr, uid, [('purchase_id', 'in', [x.id for x in inv.purchase_ids]), ('state', 'in', ['open', 'draft'])], order='date desc', context=context)
129 if not co_ids:138 if not co_ids:
130 continue139 continue
@@ -133,27 +142,50 @@
133 # Do not take invoice line that have no order_line_id (so that are not linked to a purchase order line)142 # Do not take invoice line that have no order_line_id (so that are not linked to a purchase order line)
134 if not invl.order_line_id and not inv.is_merged_by_account:143 if not invl.order_line_id and not inv.is_merged_by_account:
135 continue144 continue
136145 # exclude push flow
137 # Fetch purchase order line account146 if invl.order_line_id and (invl.order_line_id.order_id.push_fo or invl.order_line_id.set_as_sourced_n):
138 if inv.is_merged_by_account:147 continue
139 if not invl.account_id:148 old_cv_version = True
140 continue149 # CV STARTING FROM VERSION 2
141 # US-357: lines without product (get directly account)150 amount_to_subtract = invl.price_subtotal or 0.0
142 a = invl.account_id.id151 for cv_line in invl.cv_line_ids:
143 else:152 old_cv_version = False # the field cv_line_ids exist for CVs starting from version 2
144 pol = invl.order_line_id153 if abs(amount_to_subtract) <= 10**-3:
145 a = self._get_expense_account(cr, uid, pol, context=context)154 break
146 if pol.product_id and not a:155 cvl_amount_left = cv_line.amount or 0.0
147 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') % (pol.product_id.name, pol.product_id.id))156 if cvl_amount_left:
148 elif not a:157 if cv_line.id not in grouped_invl_by_cvl:
149 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this PO line: "%s" (id:%d)') % (pol.line_number, pol.id))158 grouped_invl_by_cvl[cv_line.id] = 0
150 if a not in grouped_invl:159 if amount_to_subtract >= cvl_amount_left:
151 grouped_invl[a] = 0160 grouped_invl_by_cvl[cv_line.id] += cvl_amount_left
152161 amount_to_subtract -= cvl_amount_left
153 grouped_invl[a] += invl.price_subtotal162 else:
163 grouped_invl_by_cvl[cv_line.id] += amount_to_subtract
164 amount_to_subtract = 0
165 # CV IN VERSION 1
166 if old_cv_version:
167 # Fetch purchase order line account
168 if inv.is_merged_by_account:
169 if not invl.account_id:
170 continue
171 # US-357: lines without product (get directly account)
172 a = invl.account_id.id
173 else:
174 pol = invl.order_line_id
175 a = self._get_expense_account(cr, uid, pol, context=context)
176 if pol.product_id and not a:
177 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this product: "%s" (id:%d)') %
178 (pol.product_id.name, pol.product_id.id))
179 elif not a:
180 raise osv.except_osv(_('Error !'), _('There is no expense account defined for this PO line: "%s" (id:%d)') %
181 (pol.line_number, pol.id))
182 if a not in grouped_invl_by_acc:
183 grouped_invl_by_acc[a] = 0
184 grouped_invl_by_acc[a] += invl.price_subtotal
154185
155 po_ids = [x.id for x in inv.purchase_ids]186 po_ids = [x.id for x in inv.purchase_ids]
156 self._update_commitments_lines(cr, uid, po_ids, grouped_invl, from_cancel=False, context=context)187 self._update_commitments_lines(cr, uid, po_ids, account_amount_dic=grouped_invl_by_acc,
188 cvl_amount_dic=grouped_invl_by_cvl, from_cancel=False, context=context)
157189
158 return True190 return True
159191
@@ -170,38 +202,51 @@
170202
171 return account_id203 return account_id
172204
173 def _update_commitments_lines(self, cr, uid, po_ids, account_amount_dic, from_cancel=False, context=None):205 def _update_commitments_lines(self, cr, uid, po_ids, account_amount_dic=None, cvl_amount_dic=None, from_cancel=False, context=None):
174 """206 """
175 po_ids: list of PO ids207 po_ids: list of PO ids
176 account_amount_dic: dict, keys are G/L account_id, values are amount to deduce208 account_amount_dic: dict, keys are G/L account_id, values are amount to deduce
177209
178210
179 """211 """
180 if not po_ids or not account_amount_dic:212 if not po_ids or not account_amount_dic and not cvl_amount_dic:
181 return True213 return True
182214
183 if context is None:215 if context is None:
184 context = {}216 context = {}
217 if account_amount_dic is None:
218 account_amount_dic = {}
219 if cvl_amount_dic is None:
220 cvl_amount_dic = {}
221 wf_service = netsvc.LocalService("workflow")
185222
186 # po is state=cancel on last IN cancel223 # po is state=cancel on last IN cancel
187 company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id224 company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
188 cr.execute('''select l.id, l.account_id, l.commit_id, c.state, l.amount, l.analytic_distribution_id, c.analytic_distribution_id, c.id, c.currency_id, c.type from225 # avoids empty lists so that the SQL request can be executed
226 account_list = account_amount_dic.keys() or [0]
227 cvl_list = cvl_amount_dic.keys() or [0]
228 cr.execute('''select l.id, l.account_id, l.commit_id, c.state, l.amount, l.analytic_distribution_id, c.analytic_distribution_id,
229 c.id, c.currency_id, c.type, c.version from
189 account_commitment_line l, account_commitment c230 account_commitment_line l, account_commitment c
190 where l.commit_id = c.id and231 where l.commit_id = c.id and
191 l.amount > 0 and232 l.amount > 0 and
192 c.purchase_id in %s and233 c.purchase_id in %s and
193 l.account_id in %s and234 (c.version < 2 and l.account_id in %s or c.version >= 2 and l.id in %s) and
194 c.state in ('open', 'draft')235 c.state in ('open', 'draft')
195 order by c.date asc236 order by c.date asc
196 ''', (tuple(po_ids), tuple(account_amount_dic.keys()))237 ''', (tuple(po_ids), tuple(account_list), tuple(cvl_list))
197 )238 )
198 # sort all cv lines by account / cv date239 # sort all cv lines by account / cv date
199 cv_info = {}240 account_info = {}
241 cvl_info = {}
200 auto_cv = True242 auto_cv = True
201 for cv in cr.fetchall():243 for cv in cr.fetchall():
202 if cv[1] not in cv_info:244 if cv[10] < 2:
203 cv_info[cv[1]] = []245 # CV IN VERSION 1
204 cv_info[cv[1]].append(cv)246 account_info.setdefault(cv[1], []).append(cv) # key = account id
247 else:
248 # CV STARTING FROM VERSION 2
249 cvl_info.setdefault(cv[0], []).append(cv) # key = CV line id
205 if cv[9] == 'manual':250 if cv[9] == 'manual':
206 auto_cv = False251 auto_cv = False
207252
@@ -210,76 +255,74 @@
210 cv_to_close = {}255 cv_to_close = {}
211256
212 # deduce amount on oldest cv lines257 # deduce amount on oldest cv lines
213 for account in account_amount_dic.keys():258 # NOTE: account_amount_dic is for CV in version 1 based on accounts (acc),
214 if account not in cv_info:259 # cvl_amount_dic is for CV from version 2 based on CV lines (cvl)
215 continue260 for c_type in ("acc", "cvl"):
216 for cv_line in cv_info[account]:261 if c_type == "acc":
217 if cv_line[3] == 'draft' and cv_line[2] not in draft_opened and not from_cancel:262 cv_info = account_info.copy()
218 draft_opened.append(cv_line[2])263 amount_dic = account_amount_dic.copy()
219 # If Commitment voucher in draft state we change it to 'validated' without using workflow and engagement lines generation264 else:
220 # NB: This permits to avoid modification on commitment voucher when receiving some goods265 cv_info = cvl_info.copy()
221 self.pool.get('account.commitment').write(cr, uid, [cv_line[2]], {'state': 'open'}, context=context)266 amount_dic = cvl_amount_dic.copy()
222267 for k in amount_dic.keys(): # account id or CV line id
223 if cv_line[4] - account_amount_dic[account] > 0.001:268 if k not in cv_info:
224 # update amount left on CV line269 continue
225 amount_left = cv_line[4] - account_amount_dic[account]270 for cv_line in cv_info[k]:
226 self.pool.get('account.commitment.line').write(cr, uid, [cv_line[0]], {'amount': amount_left}, context=context)271 if cv_line[3] == 'draft' and cv_line[2] not in draft_opened and not from_cancel:
227272 draft_opened.append(cv_line[2])
228 # update AAL273 # Change Draft CV to Validated State, in order to avoid CV modification when receiving some goods.
229 distrib_id = cv_line[5] or cv_line[6]274 # The workflow is used so that all the engagement lines are generated, even those which are not
230 if not distrib_id:275 # affected by the current update (e.g. partial reception + SI validation on one in 2 products).
231 raise osv.except_osv(_('Error'), _('No analytic distribution found.'))276 wf_service.trg_validate(uid, 'account.commitment', cv_line[2], 'commitment_open', cr)
232277
233 # Browse distribution278 if cv_line[4] - amount_dic[k] > 0.001:
234 distrib = self.pool.get('analytic.distribution').browse(cr, uid, [distrib_id], context=context)[0]279 # update amount left on CV line
235 engagement_lines = distrib.analytic_lines280 amount_left = cv_line[4] - amount_dic[k]
236 for distrib_lines in [distrib.cost_center_lines, distrib.funding_pool_lines, distrib.free_1_lines, distrib.free_2_lines]:281 self.pool.get('account.commitment.line').write(cr, uid, [cv_line[0]], {'amount': amount_left}, context=context)
237 for distrib_line in distrib_lines:282 # update AAL
238 vals = {283 distrib_id = cv_line[5] or cv_line[6]
239 'account_id': distrib_line.analytic_id.id,284 if not distrib_id:
240 'general_account_id': account,285 raise osv.except_osv(_('Error'), _('No analytic distribution found.'))
241 }286 # Browse distribution
242 if distrib_line._name == 'funding.pool.distribution.line':287 distrib = self.pool.get('analytic.distribution').browse(cr, uid, [distrib_id], context=context)[0]
243 vals.update({'cost_center_id': distrib_line.cost_center_id and distrib_line.cost_center_id.id or False,})288 engagement_lines = distrib.analytic_lines
289 for distrib_line in distrib.funding_pool_lines:
244 # Browse engagement lines to found out matching elements290 # Browse engagement lines to found out matching elements
245 for i in range(0,len(engagement_lines)):291 for i in range(0, len(engagement_lines)):
246 if engagement_lines[i]:292 if engagement_lines[i]:
247 eng_line = engagement_lines[i]293 eng_line = engagement_lines[i]
248 cmp_vals = {294 # restrict to the current CV line only
249 'account_id': eng_line.account_id.id,295 if eng_line.commitment_line_id and eng_line.commitment_line_id.id == cv_line[0]:
250 'general_account_id': eng_line.general_account_id.id,296 eng_line_distrib_id = eng_line.distrib_line_id and \
251 }297 eng_line.distrib_line_id._name == 'funding.pool.distribution.line' and \
252 if eng_line.cost_center_id:298 eng_line.distrib_line_id.id or False
253 cmp_vals.update({'cost_center_id': eng_line.cost_center_id.id})299 # in case of an AD with several lines, several AJIs are linked to the same CV line:
254 if cmp_vals == vals:300 # the comparison is used to decrement the right one
255 # Update analytic line with new amount301 if eng_line_distrib_id == distrib_line.id:
256 anal_amount = (distrib_line.percentage * amount_left) / 100302 # Update analytic line with new amount
257 curr_date = currency_date.get_date(self, cr, eng_line.document_date, eng_line.date,303 anal_amount = (distrib_line.percentage * amount_left) / 100
258 source_date=eng_line.source_date)304 curr_date = currency_date.get_date(self, cr, eng_line.document_date, eng_line.date,
259 context.update({'currency_date': curr_date})305 source_date=eng_line.source_date)
260 amount = -1 * self.pool.get('res.currency').compute(cr, uid, cv_line[8], company_currency,306 context.update({'currency_date': curr_date})
261 anal_amount, round=False, context=context)307 amount = -1 * self.pool.get('res.currency').compute(cr, uid, cv_line[8], company_currency,
262308 anal_amount, round=False, context=context)
263 # write new amount to corresponding engagement line309 # write new amount to corresponding engagement line
264 self.pool.get('account.analytic.line').write(cr, uid, [eng_line.id],310 self.pool.get('account.analytic.line').write(cr, uid, [eng_line.id],
265 {'amount': amount, 'amount_currency': -1 * anal_amount}, context=context)311 {'amount': amount,
266312 'amount_currency': -1 * anal_amount}, context=context)
267 # check next G/L account313 # check next G/L account or CV line
268 break314 break
269315 cv_to_close[cv_line[2]] = True
270 cv_to_close[cv_line[2]] = True316 eng_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', '=', cv_line[0])], context=context)
271 eng_ids = self.pool.get('account.analytic.line').search(cr, uid, [('commitment_line_id', '=', cv_line[0])], context=context)317 if eng_ids:
272 if eng_ids:318 self.pool.get('account.analytic.line').unlink(cr, uid, eng_ids, context=context)
273 self.pool.get('account.analytic.line').unlink(cr, uid, eng_ids, context=context)319 self.pool.get('account.commitment.line').write(cr, uid, [cv_line[0]], {'amount': 0.0}, context=context)
274 self.pool.get('account.commitment.line').write(cr, uid, [cv_line[0]], {'amount': 0.0}, context=context)320 if abs(cv_line[4] - amount_dic[k]) < 0.001:
275 if abs(cv_line[4] - account_amount_dic[account]) < 0.001:321 # check next G/L account or CV line
276 # check next G/L account322 break
277 break323 amount_dic[k] -= cv_line[4]
278324
279 # check next CV on this account325 if auto_cv and from_cancel and from_cancel is not True:
280 account_amount_dic[account] -= cv_line[4]
281
282 if auto_cv and from_cancel:
283 # we cancel the last IN from PO and no draft invoice exist326 # we cancel the last IN from PO and no draft invoice exist
284 if not self.pool.get('account.invoice').search_exist(cr, uid, [('purchase_ids', 'in', po_ids), ('state', '=', 'draft')], context=context):327 if not self.pool.get('account.invoice').search_exist(cr, uid, [('purchase_ids', 'in', po_ids), ('state', '=', 'draft')], context=context):
285 dpo_ids = self.pool.get('purchase.order').search(cr, uid, [('id', 'in', po_ids), ('po_version', '!=', 1), ('order_type', '=', 'direct')], context=context)328 dpo_ids = self.pool.get('purchase.order').search(cr, uid, [('id', 'in', po_ids), ('po_version', '!=', 1), ('order_type', '=', 'direct')], context=context)
286329
=== modified file 'bin/addons/analytic_distribution_supply/stock.py'
--- bin/addons/analytic_distribution_supply/stock.py 2018-09-04 17:49:19 +0000
+++ bin/addons/analytic_distribution_supply/stock.py 2021-08-11 08:19:49 +0000
@@ -23,30 +23,6 @@
2323
24from osv import osv24from osv import osv
2525
26class stock_picking(osv.osv):
27 _name = 'stock.picking'
28 _inherit = 'stock.picking'
29
30
31 def _invoice_hook(self, cr, uid, picking, invoice_id):
32 """
33 Create a link between invoice and purchase_order.
34 Copy analytic distribution from purchase order to invoice (or from commitment voucher if exists)
35 """
36 if invoice_id and picking:
37 po_id = picking.purchase_id and picking.purchase_id.id or False
38 so_id = picking.sale_id and picking.sale_id.id or False
39 if po_id:
40 self.pool.get('purchase.order').write(cr, uid, [po_id], {'invoice_ids': [(4, invoice_id)]})
41 if so_id:
42 self.pool.get('sale.order').write(cr, uid, [so_id], {'invoice_ids': [(4, invoice_id)]})
43 # Copy analytic distribution from purchase order or commitment voucher (if exists) or sale order
44 self.pool.get('account.invoice').fetch_analytic_distribution(cr, uid, [invoice_id])
45 return super(stock_picking, self)._invoice_hook(cr, uid, picking, invoice_id)
46
47# action_invoice_create method have been removed because of impossibility to retrieve DESTINATION from SO.
48
49stock_picking()
5026
51class stock_move(osv.osv):27class stock_move(osv.osv):
52 _name = 'stock.move'28 _name = 'stock.move'
@@ -64,6 +40,7 @@
6440
65 inv_obj = self.pool.get('account.invoice')41 inv_obj = self.pool.get('account.invoice')
66 account_amount = {}42 account_amount = {}
43 cvl_amount = {}
67 po_ids = {}44 po_ids = {}
68 for move in self.browse(cr, uid, ids, context=context):45 for move in self.browse(cr, uid, ids, context=context):
69 # Fetch all necessary elements46 # Fetch all necessary elements
@@ -80,14 +57,21 @@
80 continue57 continue
8158
82 po_ids[move.purchase_line_id.order_id.id] = True59 po_ids[move.purchase_line_id.order_id.id] = True
83 account_id = inv_obj._get_expense_account(cr, uid, move.purchase_line_id, context=context)60 cv_line = move.purchase_line_id.cv_line_ids and move.purchase_line_id.cv_line_ids[0] or False
84 if account_id:61 cv_version = cv_line and cv_line.commit_id and cv_line.commit_id.version or 1
85 if account_id not in account_amount:62 if cv_version > 1:
86 account_amount[account_id] = 063 if cv_line.id not in cvl_amount:
87 account_amount[account_id] += round(qty * price_unit, 2)64 cvl_amount[cv_line.id] = 0
8865 cvl_amount[cv_line.id] += round(qty * price_unit, 2)
89 if account_amount and po_ids:66 else:
90 inv_obj._update_commitments_lines(cr, uid, po_ids.keys(), account_amount, from_cancel=ids, context=context)67 account_id = inv_obj._get_expense_account(cr, uid, move.purchase_line_id, context=context)
68 if account_id:
69 if account_id not in account_amount:
70 account_amount[account_id] = 0
71 account_amount[account_id] += round(qty * price_unit, 2)
72 if (account_amount or cvl_amount) and po_ids:
73 inv_obj._update_commitments_lines(cr, uid, po_ids.keys(), account_amount_dic=account_amount, cvl_amount_dic=cvl_amount,
74 from_cancel=ids, context=context)
9175
92 return super(stock_move, self).action_cancel(cr, uid, ids, context=context)76 return super(stock_move, self).action_cancel(cr, uid, ids, context=context)
9377
9478
=== modified file 'bin/addons/base/res/res_log.py'
--- bin/addons/base/res/res_log.py 2018-08-06 13:07:33 +0000
+++ bin/addons/base/res/res_log.py 2021-08-11 08:19:49 +0000
@@ -55,6 +55,8 @@
55 create_context = context and dict(context) or {}55 create_context = context and dict(context) or {}
56 if 'res_log_read' in create_context:56 if 'res_log_read' in create_context:
57 vals['read'] = create_context.pop('res_log_read')57 vals['read'] = create_context.pop('res_log_read')
58 if '__copy_data_seen' in create_context:
59 create_context.pop('__copy_data_seen')
58 if create_context and not vals.get('context'):60 if create_context and not vals.get('context'):
59 vals['context'] = create_context61 vals['context'] = create_context
60 return super(res_log, self).create(cr, uid, vals, context=context)62 return super(res_log, self).create(cr, uid, vals, context=context)
6163
=== modified file 'bin/addons/delivery_mechanism/delivery_mechanism.py'
=== modified file 'bin/addons/msf_profile/data/patches.xml'
--- bin/addons/msf_profile/data/patches.xml 2021-08-05 13:10:04 +0000
+++ bin/addons/msf_profile/data/patches.xml 2021-08-11 08:19:49 +0000
@@ -677,6 +677,7 @@
677 <field name="method">us_8753_admin_never_expire_password</field>677 <field name="method">us_8753_admin_never_expire_password</field>
678 </record>678 </record>
679679
680<<<<<<< TREE
680 <!-- UF22.0 -->681 <!-- UF22.0 -->
681 <record id="us_8805_product_set_archived" model="patch.scripts">682 <record id="us_8805_product_set_archived" model="patch.scripts">
682 <field name="method">us_8805_product_set_archived</field>683 <field name="method">us_8805_product_set_archived</field>
@@ -686,5 +687,11 @@
686 <field name="method">us_8869_remove_ir_import</field>687 <field name="method">us_8869_remove_ir_import</field>
687 </record>688 </record>
688689
690=======
691 <!-- UF22.0 -->
692 <record id="us_7449_set_cv_version" model="patch.scripts">
693 <field name="method">us_7449_set_cv_version</field>
694 </record>
695>>>>>>> MERGE-SOURCE
689 </data>696 </data>
690</openerp>697</openerp>
691698
=== modified file 'bin/addons/msf_profile/i18n/fr_MF.po'
--- bin/addons/msf_profile/i18n/fr_MF.po 2021-08-11 05:45:06 +0000
+++ bin/addons/msf_profile/i18n/fr_MF.po 2021-08-11 08:19:49 +0000
@@ -3925,6 +3925,7 @@
3925#: field:account.invoice.tax,manual:03925#: field:account.invoice.tax,manual:0
3926#: view:account.commitment:03926#: view:account.commitment:0
3927#: selection:account.commitment,type:03927#: selection:account.commitment,type:0
3928#: selection:account.commitment.line,commit_type:0
3928#: selection:purchase.order,invoice_method:03929#: selection:purchase.order,invoice_method:0
3929#: model:ir.ui.menu,name:sync_client.sync_wiz_menu3930#: model:ir.ui.menu,name:sync_client.sync_wiz_menu
3930msgid "Manual"3931msgid "Manual"
@@ -9634,6 +9635,7 @@
96349635
9635#. module: analytic_distribution9636#. module: analytic_distribution
9636#: selection:account.commitment,type:09637#: selection:account.commitment,type:0
9638#: selection:account.commitment.line,commit_type:0
9637msgid "Manual - ESC supplier"9639msgid "Manual - ESC supplier"
9638msgstr "Manuel - Fournisseur ESC"9640msgstr "Manuel - Fournisseur ESC"
96399641
@@ -30297,7 +30299,7 @@
30297#: code:addons/purchase/purchase_order_line.py:179530299#: code:addons/purchase/purchase_order_line.py:1795
30298#, python-format30300#, python-format
30299msgid "There is no expense account defined for this product: \"%s\" (id:%d)"30301msgid "There is no expense account defined for this product: \"%s\" (id:%d)"
30300msgstr "Il n'y a pas de compte de charge définit pour ce produit : \"%s\" (id. : %d)"30302msgstr "Il n'y a pas de compte de charge défini pour ce produit : \"%s\" (id. : %d)"
3030130303
30302#. module: msf_outgoing30304#. module: msf_outgoing
30303#: report:packing.list:030305#: report:packing.list:0
@@ -30674,7 +30676,7 @@
30674#: code:addons/analytic_distribution_supply/invoice.py:14730676#: code:addons/analytic_distribution_supply/invoice.py:147
30675#, python-format30677#, python-format
30676msgid "There is no expense account defined for this PO line: \"%s\" (id:%d)"30678msgid "There is no expense account defined for this PO line: \"%s\" (id:%d)"
30677msgstr "There is no expense account defined for this PO line: \"%s\" (id:%d)"30679msgstr "Il n'y a pas de compte de charge défini pour cette ligne de BC : \"%s\" (id. : %d)"
3067830680
30679#. module: msf_doc_import30681#. module: msf_doc_import
30680#: view:msf.import.export:030682#: view:msf.import.export:0
@@ -44516,12 +44518,6 @@
44516msgid "Choose day"44518msgid "Choose day"
44517msgstr "Choisir un jour"44519msgstr "Choisir un jour"
4451844520
44519#. module: analytic_distribution
44520#: code:addons/analytic_distribution/account_commitment.py:161
44521#, python-format
44522msgid "No analytic distribution found for %s %s"
44523msgstr "Pas de distribution analytique trouvée pour %s %s"
44524
44525#. modules: sync_so, analytic_distribution_supply, analytic_override, analytic_distribution44521#. modules: sync_so, analytic_distribution_supply, analytic_override, analytic_distribution
44526#: code:addons/analytic_distribution/wizard/analytic_distribution_wizard.py:32244522#: code:addons/analytic_distribution/wizard/analytic_distribution_wizard.py:322
44527#: code:addons/analytic_distribution/wizard/analytic_distribution_wizard.py:35244523#: code:addons/analytic_distribution/wizard/analytic_distribution_wizard.py:352
@@ -47492,6 +47488,7 @@
47492#: view:account.bank.statement.line:047488#: view:account.bank.statement.line:0
47493#: view:sale.order.line:047489#: view:sale.order.line:0
47494#: view:free.allocation.wizard:047490#: view:free.allocation.wizard:0
47491#: view:account.commitment.line:0
47495msgid "Delete"47492msgid "Delete"
47496msgstr "Supprimer"47493msgstr "Supprimer"
4749747494
@@ -68111,7 +68108,7 @@
68111#: code:addons/analytic_distribution_supply/invoice.py:22768108#: code:addons/analytic_distribution_supply/invoice.py:227
68112#, python-format68109#, python-format
68113msgid "No analytic distribution found."68110msgid "No analytic distribution found."
68114msgstr "Pas de disribution analytique trouvée."68111msgstr "Pas de distribution analytique trouvée."
6811568112
68116#. module: base68113#. module: base
68117#: model:res.currency,currency_name:base.CHF68114#: model:res.currency,currency_name:base.CHF
@@ -69423,9 +69420,32 @@
6942369420
69424#. module: analytic_distribution69421#. module: analytic_distribution
69425#: selection:account.commitment,type:069422#: selection:account.commitment,type:0
69423#: selection:account.commitment.line,commit_type:0
69426msgid "Automatic - External supplier"69424msgid "Automatic - External supplier"
69427msgstr "Automatique - Fournisseur Externe"69425msgstr "Automatique - Fournisseur Externe"
6942869426
69427#. module: analytic_distribution
69428#: selection:account.commitment,type:0
69429#: selection:account.commitment.line,commit_type:0
69430msgid "Automatic - Intermission"
69431msgstr "Automatique - Intermission"
69432
69433#. module: analytic_distribution
69434#: selection:account.commitment,type:0
69435#: selection:account.commitment.line,commit_type:0
69436msgid "Automatic - Intersection"
69437msgstr "Automatique - Intersection"
69438
69439#. module: analytic_distribution
69440#: view:account.commitment:0
69441msgid "Intermission Commitment Voucher"
69442msgstr "Bon d'Engagement Intermission"
69443
69444#. module: analytic_distribution
69445#: view:account.commitment:0
69446msgid "Intersection Commitment Voucher"
69447msgstr "Bon d'Engagement Intersection"
69448
69429#. module: procurement69449#. module: procurement
69430#: field:procurement.order,close_move:069450#: field:procurement.order,close_move:0
69431msgid "Close Move at end"69451msgid "Close Move at end"
@@ -82184,7 +82204,7 @@
82184msgid "SFTP connection succeeded"82204msgid "SFTP connection succeeded"
82185msgstr "SFTP connection succeeded"82205msgstr "SFTP connection succeeded"
8218682206
82187#. modules: tender_flow, product_nomenclature, product_asset, account_override, product_attributes, register_accounting, product_expiry, procurement_cycle, return_claim, supplier_catalogue, import_data, mission_stock, unifield_setup, stock_forecast, stock_batch_recall, order_types, msf_doc_import, purchase_followup, product, stock_override, stock_schedule, service_purchasing, consumption_calculation, purchase_override, specific_rules, kit, base, product_list, product_manufacturer, procurement_report, threshold_value, purchase, account, msf_outgoing, stock_move_tracking, purchase_allocation_report, procurement_auto, sale, transport_mgmt, procurement, sourcing, msf_audittrail, purchase_msf, stock, sync_so, msf_tools82207#. modules: tender_flow, product_nomenclature, product_asset, account_override, product_attributes, register_accounting, product_expiry, procurement_cycle, return_claim, supplier_catalogue, import_data, mission_stock, unifield_setup, stock_forecast, stock_batch_recall, order_types, msf_doc_import, purchase_followup, product, stock_override, stock_schedule, service_purchasing, consumption_calculation, purchase_override, specific_rules, kit, base, product_list, product_manufacturer, procurement_report, threshold_value, purchase, account, msf_outgoing, stock_move_tracking, purchase_allocation_report, procurement_auto, sale, transport_mgmt, procurement, sourcing, msf_audittrail, purchase_msf, stock, sync_so, msf_tools, analytic_distribution
82188#: field:account.analytic.line,product_id:082208#: field:account.analytic.line,product_id:0
82189#: view:account.entries.report:082209#: view:account.entries.report:0
82190#: field:account.entries.report,product_id:082210#: field:account.entries.report,product_id:0
@@ -82390,6 +82410,7 @@
82390#: view:replenishment.segment.line.amc.past_fmc:082410#: view:replenishment.segment.line.amc.past_fmc:0
82391#: view:replenishment.segment.line.min_max_auto_supply.history:082411#: view:replenishment.segment.line.min_max_auto_supply.history:0
82392#: field:view.expired.expiring.stock.lines,product_id:082412#: field:view.expired.expiring.stock.lines,product_id:0
82413#: field:account.commitment.line,po_line_product_id:0
82393#, python-format82414#, python-format
82394msgid "Product"82415msgid "Product"
82395msgstr "Produit"82416msgstr "Produit"
@@ -87510,7 +87531,7 @@
87510msgstr "Ceci est utilisé uniquement si vous sélectionnez une localisation de type chainée.\n"87531msgstr "Ceci est utilisé uniquement si vous sélectionnez une localisation de type chainée.\n"
87511"La valeur 'Mouvement automatique' créera un mouvement de stock après le mouvement actuel qui sera validé automatiquement. La valeur 'Opération manuelle', le mouvement de stock devra être validé par un opérateur. La valeur 'Automatique sans étape supplémentaire', la localisation est remplacée sur le mouvement d'origine."87532"La valeur 'Mouvement automatique' créera un mouvement de stock après le mouvement actuel qui sera validé automatiquement. La valeur 'Opération manuelle', le mouvement de stock devra être validé par un opérateur. La valeur 'Automatique sans étape supplémentaire', la localisation est remplacée sur le mouvement d'origine."
8751287533
87513#. modules: msf_budget, sync_client, update_client, kit, base, msf_profile87534#. modules: msf_budget, sync_client, update_client, kit, base, msf_profile, analytic_distribution
87514#: view:ir.module.module:087535#: view:ir.module.module:0
87515#: report:ir.module.reference:087536#: report:ir.module.reference:0
87516#: view:composition.kit:087537#: view:composition.kit:0
@@ -87528,6 +87549,7 @@
87528#: field:sync.client.update_to_send,version:087549#: field:sync.client.update_to_send,version:0
87529#: field:sync.version.instance.monitor,version:087550#: field:sync.version.instance.monitor,version:0
87530#: field:sync_client.version,name:087551#: field:sync_client.version,name:0
87552#: field:account.commitment,version:0
87531msgid "Version"87553msgid "Version"
87532msgstr "Version"87554msgstr "Version"
8753387555
@@ -91254,13 +91276,21 @@
91254msgid "Purchase Orders Waiting Confirmation"91276msgid "Purchase Orders Waiting Confirmation"
91255msgstr "Bons de Commandes en attente de Confirmation"91277msgstr "Bons de Commandes en attente de Confirmation"
9125691278
91257#. modules: purchase, analytic_distribution, purchase_override91279#. modules: purchase, analytic_distribution, purchase_override, account_override, register_accounting
91258#: field:account.commitment,line_ids:091280#: field:account.commitment,line_ids:0
91259#: view:account.commitment.line:091281#: view:account.commitment.line:0
91282#: field:purchase.order.line,cv_line_ids:0
91283#: field:purchase.order.merged.line,cv_line_ids:0
91284#: field:account.invoice.line,cv_line_ids:0
91285#: field:wizard.account.invoice.line,cv_line_ids:0
91286msgid "Commitment Voucher Lines"
91287msgstr "Lignes de Bon d'Engagement"
91288
91289#. modules: purchase, purchase_override
91260#: field:purchase.order.line,commitment_line_ids:091290#: field:purchase.order.line,commitment_line_ids:0
91261#: field:purchase.order.merged.line,commitment_line_ids:091291#: field:purchase.order.merged.line,commitment_line_ids:0
91262msgid "Commitment Voucher Lines"91292msgid "Commitment Voucher Lines (deprecated)"
91263msgstr "Lignes de Bon d'Engagement"91293msgstr "Lignes de Bon d'Engagement (obsolète)"
9126491294
91265#. module: sync_client91295#. module: sync_client
91266#: code:addons/sync_client/hq_monitor.py:4891296#: code:addons/sync_client/hq_monitor.py:48
@@ -98351,13 +98381,17 @@
98351msgstr "Type de note"98381msgstr "Type de note"
9835298382
98353#. modules: purchase, analytic_distribution, msf_doc_import, finance98383#. modules: purchase, analytic_distribution, msf_doc_import, finance
98354#: field:account.commitment.line,purchase_order_line_ids:0
98355#: view:purchase.order.line:098384#: view:purchase.order.line:0
98356#: field:wizard.import.po,line_ids:098385#: field:wizard.import.po,line_ids:0
98357#: field:account.invoice.line,purchase_order_line_ids:098386#: field:account.invoice.line,purchase_order_line_ids:0
98358msgid "Purchase Order Lines"98387msgid "Purchase Order Lines"
98359msgstr "Lignes Bon de Commande"98388msgstr "Lignes Bon de Commande"
9836098389
98390#. module: analytic_distribution
98391#: field:account.commitment.line,purchase_order_line_ids:0
98392msgid "Purchase Order Lines (deprecated)"
98393msgstr "Lignes Bon de Commande (obsolète)"
98394
98361#. module: specific_rules98395#. module: specific_rules
98362#: field:unconsistent.stock.report.line,product_bn:098396#: field:unconsistent.stock.report.line,product_bn:0
98363msgid "BN management"98397msgid "BN management"
@@ -112379,6 +112413,7 @@
112379#, python-format112413#, python-format
112380msgid "The Pre-Packing List is using a deactivated Delivery Address (%s). Please select another one to be able to process."112414msgid "The Pre-Packing List is using a deactivated Delivery Address (%s). Please select another one to be able to process."
112381msgstr "La Liste de Pré-Colisage utilise une Addresse de Livraison désactivée (%s). Veuillez en sélectionner une autre pour pouvoir continuer le traitement."112415msgstr "La Liste de Pré-Colisage utilise une Addresse de Livraison désactivée (%s). Veuillez en sélectionner une autre pour pouvoir continuer le traitement."
112416<<<<<<< TREE
112382112417
112383#. module: sync_so112418#. module: sync_so
112384#: code:addons/sync_so/so_po_common.py:491112419#: code:addons/sync_so/so_po_common.py:491
@@ -112719,3 +112754,47 @@
112719#, python-format112754#, python-format
112720msgid "Products moved in the last: %s month%s"112755msgid "Products moved in the last: %s month%s"
112721msgstr "Mouvements de stock dans: %s dernier%s mois"112756msgstr "Mouvements de stock dans: %s dernier%s mois"
112757=======
112758
112759#. module: sync_so
112760#: code:addons/sync_so/so_po_common.py:491
112761#, python-format
112762msgid "Cannot process Document/line due to Product Code %s which does not exist in this instance"
112763msgstr "Impossible de traiter le Document/la ligne. Le Code Produit %s n'existe pas dans cette instance"
112764
112765#. module: analytic_distribution
112766#: help:account.commitment,version:0
112767msgid "Technical field to distinguish old CVs from new ones which have a different behavior."
112768msgstr "Champ technique pour distinguer les anciens Bons d'Engagement des nouveaux qui ont un comportement différent."
112769
112770#. module: analytic_distribution
112771#: field:account.commitment,display_super_done_button:0
112772msgid "Display the button allowing to always set a CV to Done"
112773msgstr "Afficher le bouton permettant de toujours passer un Bon d'Engagement en Terminé"
112774
112775#. module: analytic_distribution
112776#: field:account.commitment.line,commit_type:0
112777msgid "Commitment Voucher Type"
112778msgstr "Type de Bon d'Engagement"
112779
112780#. module: analytic_distribution
112781#: field:account.commitment.line,po_line_id:0
112782#: field:account.commitment.line,po_line_number:0
112783msgid "PO Line"
112784msgstr "Ligne du BC"
112785
112786#. module: analytic_distribution
112787#: view:account.commitment:0
112788msgid "Done (for Administrator only)"
112789msgstr "Terminé (pour l'Administrateur uniquement)"
112790
112791#. module: analytic_distribution
112792#: view:account.commitment:0
112793msgid "You are about to set this Commitment Voucher to the state \"Done\". Do you want to proceed?"
112794msgstr "Vous êtes sur le point de passer ce Bon d'Engagement à l'état \"Terminé\". Voulez-vous continuer ?"
112795
112796#. module: analytic_distribution
112797#: view:account.commitment.line:0
112798msgid "Do you really want to delete this line?"
112799msgstr "Voulez-vous vraiment supprimer cette ligne ?"
112800>>>>>>> MERGE-SOURCE
112722112801
=== modified file 'bin/addons/msf_profile/msf_profile.py'
--- bin/addons/msf_profile/msf_profile.py 2021-08-09 18:09:28 +0000
+++ bin/addons/msf_profile/msf_profile.py 2021-08-11 08:19:49 +0000
@@ -53,6 +53,7 @@
53 'model': lambda *a: 'patch.scripts',53 'model': lambda *a: 'patch.scripts',
54 }54 }
5555
56<<<<<<< TREE
56 # UF22.057 # UF22.0
57 def us_8805_product_set_archived(self, cr, uid, *a, **b):58 def us_8805_product_set_archived(self, cr, uid, *a, **b):
58 if self.pool.get('sync_client.version') and self.pool.get('sync.client.entity'):59 if self.pool.get('sync_client.version') and self.pool.get('sync.client.entity'):
@@ -80,6 +81,18 @@
80 cr.execute("update internal_request_import set file_to_import=NULL");81 cr.execute("update internal_request_import set file_to_import=NULL");
81 return True82 return True
8283
84=======
85 # UF22.0
86 def us_7449_set_cv_version(self, cr, uid, *a, **b):
87 """
88 Sets the existing Commitment Vouchers in version 1.
89 """
90 if self.pool.get('sync.client.entity'): # existing instances
91 cr.execute("UPDATE account_commitment SET version = 1")
92 self._logger.warn('Commitment Vouchers: %s CV(s) set to version 1.', cr.rowcount)
93 return True
94
95>>>>>>> MERGE-SOURCE
83 # UF21.196 # UF21.1
84 def us_8810_fake_updates(self, cr, uid, *a, **b):97 def us_8810_fake_updates(self, cr, uid, *a, **b):
85 if self.pool.get('sync.client.entity'):98 if self.pool.get('sync.client.entity'):
8699
=== modified file 'bin/addons/purchase/purchase_order.py'
--- bin/addons/purchase/purchase_order.py 2021-08-10 16:21:09 +0000
+++ bin/addons/purchase/purchase_order.py 2021-08-11 08:19:49 +0000
@@ -2887,12 +2887,21 @@
2887 ('instance_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context).company_id.instance_id.id)2887 ('instance_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context).company_id.instance_id.id)
2888 ], limit=1, context=context)2888 ], limit=1, context=context)
28892889
2890 po_partner_type = po.partner_id.partner_type
2891 if po_partner_type == 'external':
2892 cv_type = 'external'
2893 elif po_partner_type == 'section':
2894 cv_type = 'intersection'
2895 elif po_partner_type == 'intermission':
2896 cv_type = 'intermission'
2897 else:
2898 cv_type = 'manual'
2890 vals = {2899 vals = {
2891 'journal_id': engagement_ids and engagement_ids[0] or False,2900 'journal_id': engagement_ids and engagement_ids[0] or False,
2892 'currency_id': po.currency_id and po.currency_id.id or False,2901 'currency_id': po.currency_id and po.currency_id.id or False,
2893 'partner_id': po.partner_id and po.partner_id.id or False,2902 'partner_id': po.partner_id and po.partner_id.id or False,
2894 'purchase_id': po.id or False,2903 'purchase_id': po.id or False,
2895 'type': 'external' if po.partner_id.partner_type == 'external' else 'manual',2904 'type': cv_type,
2896 }2905 }
2897 # prepare some values2906 # prepare some values
2898 period_ids = get_period_from_date(self, cr, uid, cv_date, context=context)2907 period_ids = get_period_from_date(self, cr, uid, cv_date, context=context)
28992908
=== modified file 'bin/addons/purchase/purchase_order_line.py'
--- bin/addons/purchase/purchase_order_line.py 2021-08-10 16:13:59 +0000
+++ bin/addons/purchase/purchase_order_line.py 2021-08-11 08:19:49 +0000
@@ -568,8 +568,12 @@
568 # finance568 # finance
569 'analytic_distribution_id': fields.many2one('analytic.distribution', 'Analytic Distribution'),569 'analytic_distribution_id': fields.many2one('analytic.distribution', 'Analytic Distribution'),
570 'have_analytic_distribution_from_header': fields.function(_have_analytic_distribution_from_header, method=True, type='boolean', string='Header Distrib.?'),570 'have_analytic_distribution_from_header': fields.function(_have_analytic_distribution_from_header, method=True, type='boolean', string='Header Distrib.?'),
571 # for CV in version 1
571 'commitment_line_ids': fields.many2many('account.commitment.line', 'purchase_line_commitment_rel', 'purchase_id', 'commitment_id',572 'commitment_line_ids': fields.many2many('account.commitment.line', 'purchase_line_commitment_rel', 'purchase_id', 'commitment_id',
572 string="Commitment Voucher Lines", readonly=True),573 string="Commitment Voucher Lines (deprecated)", readonly=True),
574 # for CV starting from version 2
575 # note: cv_line_ids is a o2m because of the related m2o on CV lines but it should only contain one CV line
576 'cv_line_ids': fields.one2many('account.commitment.line', 'po_line_id', string="Commitment Voucher Lines"),
573 'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',577 'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',
574 selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],578 selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],
575 string="Distribution state", help="Informs from distribution state among 'none', 'valid', 'invalid."),579 string="Distribution state", help="Informs from distribution state among 'none', 'valid', 'invalid."),
@@ -1295,7 +1299,7 @@
1295 self.pool.get('product.product')._get_restriction_error(cr, uid, [pol.product_id.id],1299 self.pool.get('product.product')._get_restriction_error(cr, uid, [pol.product_id.id],
1296 {'partner_id': pol.order_id.partner_id.id}, context=context)1300 {'partner_id': pol.order_id.partner_id.id}, context=context)
12971301
1298 default.update({'state': 'draft', 'move_ids': [], 'invoiced': 0, 'invoice_lines': [], 'commitment_line_ids': [], })1302 default.update({'state': 'draft', 'move_ids': [], 'invoiced': 0, 'invoice_lines': [], 'commitment_line_ids': [], 'cv_line_ids': [], })
12991303
1300 for field in ['origin', 'move_dest_id', 'original_product', 'original_qty', 'original_price', 'original_uom', 'original_currency_id', 'modification_comment', 'sync_linked_sol', 'created_by_vi_import', 'external_ref']:1304 for field in ['origin', 'move_dest_id', 'original_product', 'original_qty', 'original_price', 'original_uom', 'original_currency_id', 'modification_comment', 'sync_linked_sol', 'created_by_vi_import', 'external_ref']:
1301 if field not in default:1305 if field not in default:
@@ -1868,14 +1872,17 @@
18681872
1869 import_commitments = self.pool.get('unifield.setup.configuration').get_config(cr, uid).import_commitments1873 import_commitments = self.pool.get('unifield.setup.configuration').get_config(cr, uid).import_commitments
1870 for pol in self.browse(cr, uid, ids, context=context):1874 for pol in self.browse(cr, uid, ids, context=context):
1871 # only create CV for external and ESC partners:1875 if pol.order_id.partner_id.partner_type == 'internal':
1872 if pol.order_id.partner_id.partner_type not in ['external', 'esc']:
1873 return False1876 return False
18741877
1875 if pol.order_id.partner_id.partner_type == 'esc' and import_commitments:1878 if pol.order_id.partner_id.partner_type == 'esc' and import_commitments:
1876 return False1879 return False
18771880
1878 if pol.order_id.order_type in ['loan', 'in_kind']:1881 if pol.order_id.order_type in ['loan', 'in_kind', 'donation_st', 'donation_exp']:
1882 return False
1883
1884 # exclude push flow (FO or FO line created first)
1885 if pol.order_id.push_fo or pol.set_as_sourced_n:
1879 return False1886 return False
18801887
1881 commitment_voucher_id = self.pool.get('account.commitment').search(cr, uid, [('purchase_id', '=', pol.order_id.id), ('state', '=', 'draft')], context=context)1888 commitment_voucher_id = self.pool.get('account.commitment').search(cr, uid, [('purchase_id', '=', pol.order_id.id), ('state', '=', 'draft')], context=context)
@@ -1886,42 +1893,63 @@
1886 raise osv.except_osv(_('Error'), _('Delivery Confirmed Date is a mandatory field.'))1893 raise osv.except_osv(_('Error'), _('Delivery Confirmed Date is a mandatory field.'))
1887 commitment_voucher_id = self.pool.get('purchase.order').create_commitment_voucher_from_po(cr, uid, [pol.order_id.id], cv_date=pol.confirmed_delivery_date, context=context)1894 commitment_voucher_id = self.pool.get('purchase.order').create_commitment_voucher_from_po(cr, uid, [pol.order_id.id], cv_date=pol.confirmed_delivery_date, context=context)
18881895
1889 # group PO line by account_id:
1890 expense_account = pol.account_4_distribution and pol.account_4_distribution.id or False1896 expense_account = pol.account_4_distribution and pol.account_4_distribution.id or False
1891 if not expense_account:1897 if not expense_account:
1892 raise osv.except_osv(_('Error'), _('There is no expense account defined for this line: %s (id:%d)') % (pol.name or '', pol.id))1898 raise osv.except_osv(_('Error'), _('There is no expense account defined for this line: %s (id:%d)') % (pol.name or '', pol.id))
18931899
1900 # in CV in version 1, PO lines are grouped by account_id. Else 1 PO line generates 1 CV line.
1901 cv_version = self.pool.get('account.commitment').read(cr, uid, commitment_voucher_id, ['version'], context=context)['version']
1902 cc_lines = []
1903 ad_header = [] # if filled in, the line itself has no AD but uses the one at header level
1894 if pol.analytic_distribution_id:1904 if pol.analytic_distribution_id:
1895 cc_lines = pol.analytic_distribution_id.cost_center_lines1905 cc_lines = pol.analytic_distribution_id.cost_center_lines
1896 else:1906 elif cv_version < 2:
1907 # in CV in version 1, if there is no AD on the PO line, the AD at PO header level is used at CV line level
1897 cc_lines = pol.order_id.analytic_distribution_id.cost_center_lines1908 cc_lines = pol.order_id.analytic_distribution_id.cost_center_lines
1909 else:
1910 ad_header = pol.order_id.analytic_distribution_id.cost_center_lines
18981911
1899 if not cc_lines:1912 if not cc_lines and not ad_header:
1900 raise osv.except_osv(_('Warning'), _('Analytic allocation is mandatory for %s on the line %s for the product %s! It must be added manually.')1913 raise osv.except_osv(_('Warning'), _('Analytic allocation is mandatory for %s on the line %s for the product %s! It must be added manually.')
1901 % (pol.order_id.name, pol.line_number, pol.product_id and pol.product_id.default_code or pol.name or ''))1914 % (pol.order_id.name, pol.line_number, pol.product_id and pol.product_id.default_code or pol.name or ''))
19021915
19031916 new_cv_line = False
1904 commit_line_id = self.pool.get('account.commitment.line').search(cr, uid, [('commit_id', '=', commitment_voucher_id), ('account_id', '=', expense_account)], context=context)1917 if cv_version > 1:
1905 if not commit_line_id: # create new commitment line1918 new_cv_line = True
1906 distrib_id = self.pool.get('analytic.distribution').create(cr, uid, {}, context=context)1919 else:
1907 commit_line_id = self.pool.get('account.commitment.line').create(cr, uid, {1920 commit_line_id = self.pool.get('account.commitment.line').search(cr, uid,
1921 [('commit_id', '=', commitment_voucher_id),
1922 ('account_id', '=', expense_account)], context=context)
1923 if not commit_line_id:
1924 new_cv_line = True
1925 if new_cv_line: # create new commitment line
1926 if ad_header: # the line has no AD itself, it uses the AD at header level
1927 distrib_id = False
1928 else:
1929 distrib_id = self.pool.get('analytic.distribution').create(cr, uid, {}, context=context)
1930 commit_line_vals = {
1908 'commit_id': commitment_voucher_id,1931 'commit_id': commitment_voucher_id,
1909 'account_id': expense_account,1932 'account_id': expense_account,
1910 'amount': pol.price_subtotal,1933 'amount': pol.price_subtotal,
1911 'initial_amount': pol.price_subtotal,1934 'initial_amount': pol.price_subtotal,
1912 'purchase_order_line_ids': [(4, pol.id)],
1913 'analytic_distribution_id': distrib_id,1935 'analytic_distribution_id': distrib_id,
1914 }, context=context)1936 }
1915 for aline in cc_lines:1937 if cv_version > 1:
1916 vals = {1938 commit_line_vals.update({'po_line_id': pol.id, })
1917 'distribution_id': distrib_id,1939 else:
1918 'analytic_id': aline.analytic_id.id,1940 commit_line_vals.update({'purchase_order_line_ids': [(4, pol.id)], })
1919 'currency_id': pol.order_id.currency_id.id,1941 commit_line_id = self.pool.get('account.commitment.line').create(cr, uid, commit_line_vals, context=context)
1920 'destination_id': aline.destination_id.id,1942 if distrib_id:
1921 'percentage': aline.percentage,1943 for aline in cc_lines:
1922 }1944 vals = {
1923 self.pool.get('cost.center.distribution.line').create(cr, uid, vals, context=context)1945 'distribution_id': distrib_id,
1924 self.pool.get('analytic.distribution').create_funding_pool_lines(cr, uid, [distrib_id], expense_account, context=context)1946 'analytic_id': aline.analytic_id.id,
1947 'currency_id': pol.order_id.currency_id.id,
1948 'destination_id': aline.destination_id.id,
1949 'percentage': aline.percentage,
1950 }
1951 self.pool.get('cost.center.distribution.line').create(cr, uid, vals, context=context)
1952 self.pool.get('analytic.distribution').create_funding_pool_lines(cr, uid, [distrib_id], expense_account, context=context)
19251953
1926 else: # update existing commitment line:1954 else: # update existing commitment line:
1927 commit_line_id = commit_line_id[0]1955 commit_line_id = commit_line_id[0]
19281956
=== modified file 'bin/addons/purchase/stock.py'
--- bin/addons/purchase/stock.py 2020-09-25 15:16:18 +0000
+++ bin/addons/purchase/stock.py 2021-08-11 08:19:49 +0000
@@ -34,58 +34,6 @@
34 'purchase_id': False,34 'purchase_id': False,
35 }35 }
3636
37 def _get_address_invoice(self, cr, uid, picking):
38 """ Gets invoice address of a partner
39 @return {'contact': address, 'invoice': address} for invoice
40 """
41 res = super(stock_picking, self)._get_address_invoice(cr, uid, picking)
42 if picking.purchase_id:
43 partner_obj = self.pool.get('res.partner')
44 partner = picking.purchase_id.partner_id or picking.address_id.partner_id
45 data = partner_obj.address_get(cr, uid, [partner.id],
46 ['contact', 'invoice'])
47 res.update(data)
48 return res
49
50 def get_currency_id(self, cursor, user, picking):
51 if picking.purchase_id:
52 return picking.purchase_id.pricelist_id.currency_id.id
53 else:
54 return super(stock_picking, self).get_currency_id(cursor, user, picking)
55
56 def _get_comment_invoice(self, cursor, user, picking):
57 if picking.purchase_id and picking.purchase_id.notes:
58 if picking.note:
59 return picking.note + '\n' + picking.purchase_id.notes
60 else:
61 return picking.purchase_id.notes
62 return super(stock_picking, self)._get_comment_invoice(cursor, user, picking)
63
64 def _get_price_unit_invoice(self, cursor, user, move_line, type):
65 if move_line.purchase_line_id:
66 return move_line.purchase_line_id.price_unit
67 return super(stock_picking, self)._get_price_unit_invoice(cursor, user, move_line, type)
68
69 def _get_discount_invoice(self, cursor, user, move_line):
70 if move_line.purchase_line_id:
71 return 0.0
72 return super(stock_picking, self)._get_discount_invoice(cursor, user, move_line)
73
74 def _get_taxes_invoice(self, cursor, user, move_line, type):
75 if move_line.purchase_line_id:
76 return [x.id for x in move_line.purchase_line_id.taxes_id]
77 return super(stock_picking, self)._get_taxes_invoice(cursor, user, move_line, type)
78
79 def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
80 if move_line.purchase_line_id:
81 return move_line.purchase_line_id.account_analytic_id.id
82 return super(stock_picking, self)._get_account_analytic_invoice(cursor, user, picking, move_line)
83
84 def _invoice_hook(self, cursor, user, picking, invoice_id):
85 purchase_obj = self.pool.get('purchase.order')
86 if picking.purchase_id:
87 purchase_obj.write(cursor, user, [picking.purchase_id.id], {'invoice_id': invoice_id,})
88 return super(stock_picking, self)._invoice_hook(cursor, user, picking, invoice_id)
8937
90stock_picking()38stock_picking()
9139
9240
=== modified file 'bin/addons/sale/stock.py'
--- bin/addons/sale/stock.py 2019-09-18 14:06:52 +0000
+++ bin/addons/sale/stock.py 2021-08-11 08:19:49 +0000
@@ -40,65 +40,6 @@
40 'sale_id': False40 'sale_id': False
41 }41 }
4242
43 def get_currency_id(self, cursor, user, picking):
44 if picking.sale_id:
45 return picking.sale_id.pricelist_id.currency_id.id
46 else:
47 return super(stock_picking, self).get_currency_id(cursor, user, picking)
48
49 def _get_payment_term(self, cursor, user, picking):
50 if picking.sale_id and picking.sale_id.payment_term:
51 return picking.sale_id.payment_term.id
52 return super(stock_picking, self)._get_payment_term(cursor, user, picking)
53
54 def _get_address_invoice(self, cursor, user, picking):
55 res = {}
56 if picking.sale_id:
57 res['contact'] = picking.sale_id.partner_order_id.id
58 res['invoice'] = picking.sale_id.partner_invoice_id.id
59 return res
60 return super(stock_picking, self)._get_address_invoice(cursor, user, picking)
61
62 def _get_comment_invoice(self, cursor, user, picking):
63 if picking.note or (picking.sale_id and picking.sale_id.note):
64 return picking.note or picking.sale_id.note
65 return super(stock_picking, self)._get_comment_invoice(cursor, user, picking)
66
67 def _get_price_unit_invoice(self, cursor, user, move_line, type):
68 if move_line.sale_line_id and move_line.sale_line_id.product_id.id == move_line.product_id.id:
69 uom_id = move_line.product_id.uom_id.id
70 uos_id = move_line.product_id.uos_id and move_line.product_id.uos_id.id or False
71 price = move_line.sale_line_id.price_unit
72 coeff = move_line.product_id.uos_coeff
73 if uom_id != uos_id and coeff != 0:
74 price_unit = price / coeff
75 return price_unit
76 return move_line.sale_line_id.price_unit
77 return super(stock_picking, self)._get_price_unit_invoice(cursor, user, move_line, type)
78
79 def _get_discount_invoice(self, cursor, user, move_line):
80 if move_line.sale_line_id:
81 return move_line.sale_line_id.discount
82 return super(stock_picking, self)._get_discount_invoice(cursor, user, move_line)
83
84 def _get_taxes_invoice(self, cursor, user, move_line, type):
85 if move_line.sale_line_id and move_line.sale_line_id.product_id.id == move_line.product_id.id:
86 return [x.id for x in move_line.sale_line_id.tax_id]
87 return super(stock_picking, self)._get_taxes_invoice(cursor, user, move_line, type)
88
89 def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
90 if picking.sale_id:
91 return picking.sale_id.project_id.id
92 return super(stock_picking, self)._get_account_analytic_invoice(cursor, user, picking, move_line)
93
94 def _invoice_hook(self, cursor, user, picking, invoice_id):
95 sale_obj = self.pool.get('sale.order')
96 if picking.sale_id:
97 sale_obj.write(cursor, user, [picking.sale_id.id], {
98 'invoice_ids': [(4, invoice_id)],
99 })
100 return super(stock_picking, self)._invoice_hook(cursor, user, picking, invoice_id)
101
10243
103stock_picking()44stock_picking()
10445
10546
=== modified file 'bin/addons/stock/stock.py'
--- bin/addons/stock/stock.py 2021-08-09 14:13:09 +0000
+++ bin/addons/stock/stock.py 2021-08-11 08:19:49 +0000
@@ -1193,12 +1193,20 @@
1193 return True1193 return True
11941194
1195 def get_currency_id(self, cr, uid, picking):1195 def get_currency_id(self, cr, uid, picking):
1196 return False1196 if picking.sale_id:
1197 return picking.sale_id.pricelist_id.currency_id.id
1198 else:
1199 if picking.purchase_id:
1200 return picking.purchase_id.pricelist_id.currency_id.id
1201 else:
1202 return False
11971203
1198 def _get_payment_term(self, cr, uid, picking):1204 def _get_payment_term(self, cr, uid, picking):
1199 """ Gets payment term from partner.1205 """ Gets payment term from partner.
1200 @return: Payment term1206 @return: Payment term
1201 """1207 """
1208 if picking.sale_id and picking.sale_id.payment_term:
1209 return picking.sale_id.payment_term.id
1202 partner = picking.address_id.partner_id1210 partner = picking.address_id.partner_id
1203 return partner.property_payment_term and partner.property_payment_term.id or False1211 return partner.property_payment_term and partner.property_payment_term.id or False
12041212
@@ -1206,37 +1214,85 @@
1206 """ Gets invoice address of a partner1214 """ Gets invoice address of a partner
1207 @return {'contact': address, 'invoice': address} for invoice1215 @return {'contact': address, 'invoice': address} for invoice
1208 """1216 """
1217 res = {}
1209 partner_obj = self.pool.get('res.partner')1218 partner_obj = self.pool.get('res.partner')
1219 if picking.sale_id:
1220 res['contact'] = picking.sale_id.partner_order_id.id
1221 res['invoice'] = picking.sale_id.partner_invoice_id.id
1222 return res
1210 partner = picking.address_id.partner_id1223 partner = picking.address_id.partner_id
1211 return partner_obj.address_get(cr, uid, [partner.id],1224 res = partner_obj.address_get(cr, uid, [partner.id], ['contact', 'invoice'])
1212 ['contact', 'invoice'])1225 if picking.purchase_id:
1226 partner = picking.purchase_id.partner_id or picking.address_id.partner_id
1227 data = partner_obj.address_get(cr, uid, [partner.id], ['contact', 'invoice'])
1228 res.update(data)
1229 return res
12131230
1214 def _get_comment_invoice(self, cr, uid, picking):1231 def _get_comment_invoice(self, cr, uid, picking):
1215 """1232 """
1216 @return: comment string for invoice1233 @return: comment string for invoice
1217 """1234 """
1235 if picking.note or (picking.sale_id and picking.sale_id.note):
1236 return picking.note or picking.sale_id.note
1237 if picking.purchase_id and picking.purchase_id.notes:
1238 if picking.note:
1239 return picking.note + '\n' + picking.purchase_id.notes
1240 else:
1241 return picking.purchase_id.notes
1218 return picking.note or ''1242 return picking.note or ''
12191243
1220 def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None):1244 def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None):
1221 """ Gets price unit for invoice1245 """ Gets price unit for invoice
1246 Updates the Unit price according to the UoM received and the UoM ordered
1222 @param move_line: Stock move lines1247 @param move_line: Stock move lines
1223 @param type: Type of invoice1248 @param type: Type of invoice
1224 @return: The price unit for the move line1249 @return: The price unit for the move line
1225 """1250 """
1226 if context is None:1251 if context is None:
1227 context = {}1252 context = {}
12281253 res = None
1229 if type in ('in_invoice', 'in_refund'):1254 if move_line.sale_line_id and move_line.sale_line_id.product_id.id == move_line.product_id.id:
1230 # Take the user company and pricetype1255 uom_id = move_line.product_id.uom_id.id
1231 context['currency_id'] = move_line.company_id.currency_id.id1256 uos_id = move_line.product_id.uos_id and move_line.product_id.uos_id.id or False
1232 amount_unit = move_line.product_id.price_get('standard_price', context)[move_line.product_id.id]1257 price = move_line.sale_line_id.price_unit
1233 return amount_unit1258 coeff = move_line.product_id.uos_coeff
1234 else:1259 if uom_id != uos_id and coeff != 0:
1235 return move_line.product_id.list_price1260 price_unit = price / coeff
1261 res = price_unit
1262 else:
1263 res = move_line.sale_line_id.price_unit
1264 if res is None:
1265 if move_line.purchase_line_id:
1266 res = move_line.purchase_line_id.price_unit
1267 else:
1268 if type in ('in_invoice', 'in_refund'):
1269 # Take the user company and pricetype
1270 context['currency_id'] = move_line.company_id.currency_id.id
1271 amount_unit = move_line.product_id.price_get('standard_price', context)[move_line.product_id.id]
1272 res = amount_unit
1273 else:
1274 res = move_line.product_id.list_price
1275 if type == 'in_refund':
1276 if move_line.picking_id and move_line.picking_id.purchase_id:
1277 po_line_obj = self.pool.get('purchase.order.line')
1278 po_line_id = po_line_obj.search(cr, uid, [('order_id', '=', move_line.picking_id.purchase_id.id),
1279 ('product_id', '=', move_line.product_id.id),
1280 ('state', '!=', 'cancel')
1281 ], limit=1)
1282 if po_line_id:
1283 return po_line_obj.read(cr, uid, po_line_id[0], ['price_unit'])['price_unit']
1284 if move_line.purchase_line_id:
1285 po_uom_id = move_line.purchase_line_id.product_uom.id
1286 move_uom_id = move_line.product_uom.id
1287 uom_ratio = self.pool.get('product.uom')._compute_price(cr, uid, move_uom_id, 1, po_uom_id)
1288 return res / uom_ratio
1289 return res
12361290
1237 def _get_discount_invoice(self, cr, uid, move_line):1291 def _get_discount_invoice(self, cr, uid, move_line):
1238 '''Return the discount for the move line'''1292 '''Return the discount for the move line'''
1239 return 0.01293 if move_line.sale_line_id:
1294 return move_line.sale_line_id.discount
1295 return 0.0 # including if move_line.purchase_line_id
12401296
1241 def _get_taxes_invoice(self, cr, uid, move_line, type):1297 def _get_taxes_invoice(self, cr, uid, move_line, type):
1242 """ Gets taxes on invoice1298 """ Gets taxes on invoice
@@ -1244,6 +1300,10 @@
1244 @param type: Type of invoice1300 @param type: Type of invoice
1245 @return: Taxes Ids for the move line1301 @return: Taxes Ids for the move line
1246 """1302 """
1303 if move_line.sale_line_id and move_line.sale_line_id.product_id.id == move_line.product_id.id:
1304 return [x.id for x in move_line.sale_line_id.tax_id]
1305 if move_line.purchase_line_id:
1306 return [x.id for x in move_line.purchase_line_id.taxes_id]
1247 if type in ('in_invoice', 'in_refund'):1307 if type in ('in_invoice', 'in_refund'):
1248 taxes = move_line.product_id.supplier_taxes_id1308 taxes = move_line.product_id.supplier_taxes_id
1249 else:1309 else:
@@ -1259,7 +1319,11 @@
1259 else:1319 else:
1260 return map(lambda x: x.id, taxes)1320 return map(lambda x: x.id, taxes)
12611321
1262 def _get_account_analytic_invoice(self, cr, uid, picking, move_line):1322 def _get_account_analytic_invoice(self, picking, move_line):
1323 if picking.sale_id:
1324 return picking.sale_id.project_id.id
1325 if move_line.purchase_line_id:
1326 return move_line.purchase_line_id.account_analytic_id.id
1263 return False1327 return False
12641328
1265 def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id, account_id):1329 def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id, account_id):
@@ -1294,9 +1358,33 @@
1294 return True1358 return True
12951359
1296 def _invoice_hook(self, cr, uid, picking, invoice_id):1360 def _invoice_hook(self, cr, uid, picking, invoice_id):
1297 '''Call after the creation of the invoice'''1361 """
1362 Create a link between invoice and purchase_order.
1363 Copy analytic distribution from purchase order to invoice (or from commitment voucher if it exists)
1364
1365 To call after the creation of the invoice
1366 """
1367 sale_obj = self.pool.get('sale.order')
1368 purchase_obj = self.pool.get('purchase.order')
1369 if invoice_id and picking:
1370 po_id = picking.purchase_id and picking.purchase_id.id or False
1371 so_id = picking.sale_id and picking.sale_id.id or False
1372 if po_id:
1373 self.pool.get('purchase.order').write(cr, uid, [po_id], {'invoice_ids': [(4, invoice_id)]})
1374 if so_id:
1375 self.pool.get('sale.order').write(cr, uid, [so_id], {'invoice_ids': [(4, invoice_id)]})
1376 # Copy analytic distribution from purchase order or commitment voucher (if it exists) or sale order
1377 self.pool.get('account.invoice').fetch_analytic_distribution(cr, uid, [invoice_id])
1378 if picking.sale_id:
1379 sale_obj.write(cr, uid, [picking.sale_id.id], {
1380 'invoice_ids': [(4, invoice_id)],
1381 })
1382 if picking.purchase_id:
1383 purchase_obj.write(cr, uid, [picking.purchase_id.id], {'invoice_id': invoice_id, })
1298 return1384 return
12991385
1386 # action_invoice_create method has been removed because of the impossibility to retrieve DESTINATION from SO.
1387
1300 def _get_invoice_type(self, pick):1388 def _get_invoice_type(self, pick):
1301 src_usage = dest_usage = None1389 src_usage = dest_usage = None
1302 inv_type = None1390 inv_type = None
@@ -1550,12 +1638,17 @@
1550 else:1638 else:
1551 name = move_line.name1639 name = move_line.name
15521640
1641 cv_line = move_line and move_line.purchase_line_id and move_line.purchase_line_id.cv_line_ids and \
1642 move_line.purchase_line_id.cv_line_ids[0] or False
1643 cv_version = cv_line and cv_line.commit_id and cv_line.commit_id.version or 1
1553 if inv_type in ('out_invoice', 'out_refund'):1644 if inv_type in ('out_invoice', 'out_refund'):
1554 account_id = move_line.product_id.product_tmpl_id.\1645 account_id = move_line.product_id.product_tmpl_id.\
1555 property_account_income.id1646 property_account_income.id
1556 if not account_id:1647 if not account_id:
1557 account_id = move_line.product_id.categ_id.\1648 account_id = move_line.product_id.categ_id.\
1558 property_account_income_categ.id1649 property_account_income_categ.id
1650 elif cv_version > 1:
1651 account_id = cv_line.account_id.id
1559 else:1652 else:
1560 account_id = move_line.product_id.product_tmpl_id.\1653 account_id = move_line.product_id.product_tmpl_id.\
1561 property_account_expense.id1654 property_account_expense.id
@@ -1567,14 +1660,15 @@
1567 move_line, inv_type)1660 move_line, inv_type)
1568 discount = self._get_discount_invoice(cr, uid, move_line)1661 discount = self._get_discount_invoice(cr, uid, move_line)
1569 tax_ids = self._get_taxes_invoice(cr, uid, move_line, inv_type)1662 tax_ids = self._get_taxes_invoice(cr, uid, move_line, inv_type)
1570 account_analytic_id = self._get_account_analytic_invoice(cr, uid, picking, move_line)1663 account_analytic_id = self._get_account_analytic_invoice(picking, move_line)
15711664
1572 #set UoS if it's a sale and the picking doesn't have one1665 #set UoS if it's a sale and the picking doesn't have one
1573 uos_id = move_line.product_uos and move_line.product_uos.id or False1666 uos_id = move_line.product_uos and move_line.product_uos.id or False
1574 if not uos_id and inv_type in ('out_invoice', 'out_refund'):1667 if not uos_id and inv_type in ('out_invoice', 'out_refund'):
1575 uos_id = move_line.product_uom.id1668 uos_id = move_line.product_uom.id
1576 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)1669 if cv_version < 2:
1577 invoice_line_id = invoice_line_obj.create(cr, uid, {1670 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
1671 inv_vals = {
1578 'name': name,1672 'name': name,
1579 'origin': origin,1673 'origin': origin,
1580 'invoice_id': invoice_id,1674 'invoice_id': invoice_id,
@@ -1586,7 +1680,10 @@
1586 'quantity': move_line.product_uos_qty or move_line.product_qty,1680 'quantity': move_line.product_uos_qty or move_line.product_qty,
1587 'invoice_line_tax_id': [(6, 0, tax_ids)],1681 'invoice_line_tax_id': [(6, 0, tax_ids)],
1588 'account_analytic_id': account_analytic_id,1682 'account_analytic_id': account_analytic_id,
1589 }, context=context)1683 }
1684 if cv_version > 1:
1685 inv_vals.update({'cv_line_ids': [(4, cv_line.id)],})
1686 invoice_line_id = invoice_line_obj.create(cr, uid, inv_vals, context=context)
1590 self._invoice_line_hook(cr, uid, move_line, invoice_line_id, account_id)1687 self._invoice_line_hook(cr, uid, move_line, invoice_line_id, account_id)
15911688
1592 if picking.sale_id:1689 if picking.sale_id:
@@ -1613,7 +1710,7 @@
1613 tax_ids = sale_line.tax_id1710 tax_ids = sale_line.tax_id
1614 tax_ids = map(lambda x: x.id, tax_ids)1711 tax_ids = map(lambda x: x.id, tax_ids)
16151712
1616 account_analytic_id = self._get_account_analytic_invoice(cr, uid, picking, sale_line)1713 account_analytic_id = self._get_account_analytic_invoice(picking, sale_line)
16171714
1618 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, picking.sale_id.partner_id.property_account_position, account_id)1715 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, picking.sale_id.partner_id.property_account_position, account_id)
1619 invoice_line_id = invoice_line_obj.create(cr, uid, {1716 invoice_line_id = invoice_line_obj.create(cr, uid, {
16201717
=== modified file 'bin/addons/stock/stock_move.py'
=== modified file 'bin/addons/stock_override/stock.py'
--- bin/addons/stock_override/stock.py 2021-08-10 16:18:43 +0000
+++ bin/addons/stock_override/stock.py 2021-08-11 08:19:49 +0000
@@ -870,29 +870,6 @@
870870
871 return res871 return res
872872
873 def _get_price_unit_invoice(self, cr, uid, move_line, type):
874 '''
875 Update the Unit price according to the UoM received and the UoM ordered
876 '''
877 res = super(stock_picking, self)._get_price_unit_invoice(cr, uid, move_line, type)
878 if type == 'in_refund':
879 if move_line.picking_id and move_line.picking_id.purchase_id:
880 po_line_obj = self.pool.get('purchase.order.line')
881 po_line_id = po_line_obj.search(cr, uid, [('order_id', '=', move_line.picking_id.purchase_id.id),
882 ('product_id', '=', move_line.product_id.id),
883 ('state', '!=', 'cancel')
884 ], limit=1)
885 if po_line_id:
886 return po_line_obj.read(cr, uid, po_line_id[0], ['price_unit'])['price_unit']
887
888 if move_line.purchase_line_id:
889 po_uom_id = move_line.purchase_line_id.product_uom.id
890 move_uom_id = move_line.product_uom.id
891 uom_ratio = self.pool.get('product.uom')._compute_price(cr, uid, move_uom_id, 1, po_uom_id)
892 return res / uom_ratio
893
894 return res
895
896 def action_confirm(self, cr, uid, ids, context=None):873 def action_confirm(self, cr, uid, ids, context=None):
897 """874 """
898 stock.picking: action confirm875 stock.picking: action confirm
899876
=== modified file 'bin/addons/sync_client/message.py'
=== modified file 'bin/addons/sync_so/picking.py'
=== modified file 'bin/addons/sync_so/purchase.py'
--- bin/addons/sync_so/purchase.py 2021-08-10 16:18:43 +0000
+++ bin/addons/sync_so/purchase.py 2021-08-11 08:19:49 +0000
@@ -332,7 +332,7 @@
332 kind = 'update'332 kind = 'update'
333 pol_to_update = [pol_updated]333 pol_to_update = [pol_updated]
334 confirmed_sequence = self.pool.get('purchase.order.line.state').get_sequence(cr, uid, [], 'confirmed', context=context)334 confirmed_sequence = self.pool.get('purchase.order.line.state').get_sequence(cr, uid, [], 'confirmed', context=context)
335 po_line = self.browse(cr, uid, pol_updated, fields_to_fetch=['state', 'product_qty'], context=context)335 po_line = self.browse(cr, uid, pol_updated, fields_to_fetch=['state', 'product_qty', 'price_unit', 'cv_line_ids'], context=context)
336 pol_state = po_line.state336 pol_state = po_line.state
337 if sol_dict['state'] in ['cancel', 'cancel_r']:337 if sol_dict['state'] in ['cancel', 'cancel_r']:
338 pol_values['cancelled_by_sync'] = True338 pol_values['cancelled_by_sync'] = True
@@ -340,6 +340,12 @@
340 # if the state is less than confirmed we update the PO line340 # if the state is less than confirmed we update the PO line
341 if debug:341 if debug:
342 logger.info("Write pol id: %s, values: %s" % (pol_to_update, pol_values))342 logger.info("Write pol id: %s, values: %s" % (pol_to_update, pol_values))
343 if po_line.cv_line_ids and po_line.cv_line_ids[0] and po_line.state == 'confirmed' and po_line.product_qty - pol_values.get('product_qty', po_line.product_qty) > 0.01:
344 # update qty on confirmed po line: update CV line if any
345 # from_cancel = True : do not trigger wkf transition draft -> open
346 self.pool.get('account.invoice')._update_commitments_lines(cr, uid, [po_ids[0]], cvl_amount_dic={
347 po_line.cv_line_ids[0].id: round((po_line.product_qty - pol_values['product_qty'])*po_line.price_unit, 2)
348 }, from_cancel=True, context=context)
343 self.pool.get('purchase.order.line').write(cr, uid, pol_to_update, pol_values, context=context)349 self.pool.get('purchase.order.line').write(cr, uid, pol_to_update, pol_values, context=context)
344350
345 if debug:351 if debug:
346352
=== modified file 'bin/addons/sync_so/sale.py'
=== modified file 'bin/addons/sync_so/so_po_common.py'
=== modified file 'bin/osv/expression.py'
--- bin/osv/expression.py 2021-08-09 18:09:28 +0000
+++ bin/osv/expression.py 2021-08-11 08:19:49 +0000
@@ -360,8 +360,13 @@
360 if field.translate:360 if field.translate:
361 if operator in ('like', 'ilike', 'not like', 'not ilike'):361 if operator in ('like', 'ilike', 'not like', 'not ilike'):
362 right = '%%%s%%' % right362 right = '%%%s%%' % right
363<<<<<<< TREE
363 if right and operator in ('like', 'ilike', 'not like', 'not ilike', '=like', '=ilike'):364 if right and operator in ('like', 'ilike', 'not like', 'not ilike', '=like', '=ilike'):
364 right = right.replace('\\', '\\\\').replace('_', '\\_')365 right = right.replace('\\', '\\\\').replace('_', '\\_')
366=======
367 if operator in ('like', 'ilike', 'not like', 'not ilike', '=like', '=ilike'):
368 right = right.replace('\\', '\\\\').replace('_', '\\_')
369>>>>>>> MERGE-SOURCE
365370
366 operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)371 operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
367372
@@ -486,8 +491,13 @@
486 elif left in table._columns:491 elif left in table._columns:
487 params = table._columns[left]._symbol_set[1](right)492 params = table._columns[left]._symbol_set[1](right)
488493
494<<<<<<< TREE
489 if params and operator in ('like', 'ilike', 'not like', 'not ilike', '=like', '=ilike'):495 if params and operator in ('like', 'ilike', 'not like', 'not ilike', '=like', '=ilike'):
490 params = params.replace('\\', '\\\\').replace('_', '\\_')496 params = params.replace('\\', '\\\\').replace('_', '\\_')
497=======
498 if operator in ('like', 'ilike', 'not like', 'not ilike', '=like', '=ilike'):
499 params = params.replace('\\', '\\\\').replace('_', '\\_')
500>>>>>>> MERGE-SOURCE
491 if add_null:501 if add_null:
492 query = '(%s OR %s IS NULL)' % (query, left)502 query = '(%s OR %s IS NULL)' % (query, left)
493503

Subscribers

People subscribed via source and target branches