diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 65bddb429c1..681d1b28df6 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1257,6 +1257,8 @@ class AccountPartialReconcile(models.Model): cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance for example). """ + # we must unlink the reconciliation with the exchange rate difference entry as well, to be able to reconcile it with its reversal entry + to_unlink = self exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)]) # revert the currency difference entry reversed_moves = exchange_rate_entries.reverse_moves() @@ -1270,10 +1272,11 @@ class AccountPartialReconcile(models.Model): if acm_line.account_id.reconcile: for origin_line in origin_move.line_ids: if origin_line.account_id == acm_line.account_id and origin_line.debit == acm_line.credit and origin_line.credit == acm_line.debit: + to_unlink |= origin_line.matched_debit_ids | origin_line.matched_credit_ids to_rec = origin_line + acm_line pairs_to_rec.append(to_rec) # the call to super() had to be delayed in order to mark the move lines to reconcile together (to use 'rate_diff_partial_rec_id') - res = super(AccountPartialReconcile, self).unlink() + res = super(AccountPartialReconcile, to_unlink).unlink() # now that the origin currency difference line is not reconciled anymore, we can reconcile it with its reversal entry to cancel it completly for to_rec in pairs_to_rec: to_rec.reconcile() diff --git a/addons/account_asset/account_asset_invoice.py b/addons/account_asset/account_asset_invoice.py index 7a0b90c3581..db020599202 100644 --- a/addons/account_asset/account_asset_invoice.py +++ b/addons/account_asset/account_asset_invoice.py @@ -73,12 +73,14 @@ class AccountInvoiceLine(models.Model): self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id def _set_additional_fields(self, invoice): - if invoice.type == 'out_invoice': - self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id - elif invoice.type == 'in_invoice': - self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id + if not self.asset_category_id: + if invoice.type == 'out_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id + elif invoice.type == 'in_invoice': + self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id super(AccountInvoiceLine, self)._set_additional_fields(invoice) + class ProductTemplate(models.Model): _inherit = 'product.template' asset_category_id = fields.Many2one('account.asset.category', string='Asset Type', ondelete="restrict") diff --git a/addons/account_full_reconcile/__init__.py b/addons/account_full_reconcile/__init__.py new file mode 100644 index 00000000000..9c0ea901d41 --- /dev/null +++ b/addons/account_full_reconcile/__init__.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import models + +from openerp import SUPERUSER_ID +from collections import OrderedDict +from openerp.tools import float_compare + +def _migrate_full_reconcile(cr, registry): + #avoid doing anything if the table has already something in it (already migrated) + cr.execute("""SELECT count(id) FROM account_full_reconcile""") + res = cr.fetchone()[0] + if res: + return + #check column reconcile_id exists on account.move.line + cr.execute(""" + SELECT 1 FROM information_schema.columns + WHERE table_name = 'account_move_line' + AND column_name = 'reconcile_id' + """) + if not cr.fetchone(): + #if no, it's a fresh v9 database and we cannot migrate anything. Rebuild the whole table based on partial + all_partial_rec_ids = registry['account.partial.reconcile'].search(cr, SUPERUSER_ID, []) + already_processed = {} + for partial in registry['account.partial.reconcile'].browse(cr, SUPERUSER_ID, all_partial_rec_ids): + partial_rec_set = OrderedDict.fromkeys([partial]) + aml_set = set() + total_debit = 0 + total_credit = 0 + for partial_rec in partial_rec_set: + if partial_rec in already_processed: + continue + for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: + if aml not in aml_set: + total_debit += aml.debit + total_credit += aml.credit + aml_set |= set([aml]) + for x in aml.matched_debit_ids | aml.matched_credit_ids: + partial_rec_set[x] = None + partial_rec_ids = [] + for x in partial_rec_set.keys(): + partial_rec_ids.append(x.id) + already_processed[x] = None + aml_ids = [x.id for x in aml_set] + if aml_ids and partial_rec_ids: + #then, if the total debit and credit are equal, the reconciliation is full + digits_rounding_precision = aml.company_id.currency_id.rounding + if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0: + #in that case, mark the reference on the partial reconciliations and the entries + registry['account.full.reconcile'].create(cr, SUPERUSER_ID, { + 'partial_reconcile_ids': [(6, 0, partial_rec_ids)], + 'reconciled_line_ids': [(6, 0, aml_ids)] + }, context={'check_move_validity': False}) + return + + #otherwise, use the table account_move_reconcile if it exists (not dropped by migration) + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name = 'account_move_reconcile'") + if cr.fetchone(): + #account_move_reconcile exists + #copy old table + cr.execute(""" + INSERT INTO account_full_reconcile (id, name, create_date) + SELECT id, name, create_date + FROM account_move_reconcile + WHERE id IN ( + SELECT DISTINCT reconcile_id FROM account_move_line WHERE reconcile_id IS NOT NULL) + """) + else: + #account_move_reconcile was dropped during migration, rebuild that table + cr.execute(""" + INSERT INTO account_full_reconcile (id, name) + SELECT DISTINCT reconcile_id, reconcile_ref FROM account_move_line WHERE reconcile_id IS NOT NULL + """) + #update the index of account_full_reconcile + cr.execute("SELECT setval('account_full_reconcile_id_seq', (SELECT MAX(id) FROM account_full_reconcile))") + + #copy values on account.move.line + cr.execute("UPDATE account_move_line SET full_reconcile_id = reconcile_id WHERE reconcile_id IS NOT NULL") + #copy the full_reconcile_id existing of account.move.line to their account.partial.reconcile + cr.execute(""" + WITH tmp_table AS ( + SELECT rec.id AS partial_id, aml.reconcile_id AS full_id + FROM account_move_line aml + RIGHT JOIN account_partial_reconcile rec + ON aml.id = rec.debit_move_id + WHERE aml.reconcile_id IS NOT NULL + UNION + SELECT rec.id AS partial_id, aml.reconcile_id AS full_id + FROM account_move_line aml + RIGHT JOIN account_partial_reconcile rec + ON aml.id = rec.credit_move_id + WHERE aml.reconcile_id IS NOT NULL) + UPDATE account_partial_reconcile p + SET full_reconcile_id = (SELECT full_id FROM tmp_table tmp WHERE p.id = tmp.partial_id) + WHERE p.id IN (SELECT partial_id FROM tmp_table) + """) diff --git a/addons/account_full_reconcile/__openerp__.py b/addons/account_full_reconcile/__openerp__.py new file mode 100644 index 00000000000..f2c54d31d9e --- /dev/null +++ b/addons/account_full_reconcile/__openerp__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Full Reconciliation Concept', + 'version': '1.1', + 'category': 'Accounting & Finance', + 'description': """ +Add the concept of full reconciliation back into the accounting. + """, + 'website': 'https://www.odoo.com/page/accounting', + 'depends': ['account'], + 'data': [ + 'security/ir.model.access.csv', + 'views/account_full_reconcile_view.xml', + ], + 'demo': [], + 'installable': True, + 'auto_install': True, + 'post_init_hook': '_migrate_full_reconcile', +} diff --git a/addons/account_full_reconcile/models/__init__.py b/addons/account_full_reconcile/models/__init__.py new file mode 100644 index 00000000000..03e58f9db59 --- /dev/null +++ b/addons/account_full_reconcile/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import account_full_reconcile diff --git a/addons/account_full_reconcile/models/account_full_reconcile.py b/addons/account_full_reconcile/models/account_full_reconcile.py new file mode 100644 index 00000000000..4b666f88371 --- /dev/null +++ b/addons/account_full_reconcile/models/account_full_reconcile.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from collections import OrderedDict +from openerp.tools import float_compare +from openerp import api, fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching Number") + + +class AccountPartialReconcile(models.Model): + _inherit = "account.partial.reconcile" + + full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile") + + @api.model + def create(self, vals): + res = super(AccountPartialReconcile, self).create(vals) + #check if the reconcilation is full + #first, gather all journal items involved in the reconciliation just created + partial_rec_set = OrderedDict.fromkeys([x for x in res]) + aml_set = self.env['account.move.line'] + total_debit = 0 + total_credit = 0 + for partial_rec in partial_rec_set: + for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: + if aml not in aml_set: + total_debit += aml.debit + total_credit += aml.credit + aml_set |= aml + for x in aml.matched_debit_ids | aml.matched_credit_ids: + partial_rec_set[x] = None + partial_rec_ids = [x.id for x in partial_rec_set.keys()] + aml_ids = [x.id for x in aml_set] + #then, if the total debit and credit are equal, the reconciliation is full + digits_rounding_precision = aml_set[0].company_id.currency_id.rounding + if float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0: + #in that case, mark the reference on the partial reconciliations and the entries + self.env['account.full.reconcile'].with_context(check_move_validity=False).create({ + 'partial_reconcile_ids': [(6, 0, partial_rec_ids)], + 'reconciled_line_ids': [(6, 0, aml_ids)]}) + return res + + @api.multi + def unlink(self): + """When removing a partial reconciliation, also unlink its full reconciliation if it exists""" + full_rec = self.env['account.full.reconcile'] + for rec in self: + if rec.full_reconcile_id: + full_rec |= rec.full_reconcile_id + full_rec.unlink() + return super(AccountPartialReconcile, self).unlink() + + +class AccountFullReconcile(models.Model): + _name = "account.full.reconcile" + _description = "Full Reconcile" + + name = fields.Char(string='Number', required=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('account.reconcile')) + partial_reconcile_ids = fields.One2many('account.partial.reconcile', 'full_reconcile_id', string='Reconciliation Parts') + reconciled_line_ids = fields.One2many('account.move.line', 'full_reconcile_id', string='Matched Journal Items') diff --git a/addons/account_full_reconcile/security/ir.model.access.csv b/addons/account_full_reconcile/security/ir.model.access.csv new file mode 100644 index 00000000000..08cd0c1b3a9 --- /dev/null +++ b/addons/account_full_reconcile/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_full_reconcile_group_invoice,account.full.reconcile.group.invoice,model_account_full_reconcile,account.group_account_invoice,1,1,1,0 +access_account_full_reconcile,account.full.reconcile,model_account_full_reconcile,account.group_account_user,1,1,1,1 diff --git a/addons/account_full_reconcile/views/account_full_reconcile_view.xml b/addons/account_full_reconcile/views/account_full_reconcile_view.xml new file mode 100644 index 00000000000..db67f4729b3 --- /dev/null +++ b/addons/account_full_reconcile/views/account_full_reconcile_view.xml @@ -0,0 +1,50 @@ + + + + + + + account.full.reconcile.form + account.full.reconcile + +
+ +
+

+
+ + +
+
+
+
+ + + account.move.line.form.full.reconcile + account.move.line + + + + + + + 1 + + + + + + + + + + +
+
+ diff --git a/addons/crm/report/crm_activity_report.py b/addons/crm/report/crm_activity_report.py index 5bf4445fc72..ae7ac6621e0 100644 --- a/addons/crm/report/crm_activity_report.py +++ b/addons/crm/report/crm_activity_report.py @@ -44,14 +44,10 @@ class crm_activity_report(models.Model): l.type as lead_type from "mail_message" m - left join + join "crm_lead" l on (m.res_id = l.id) - inner join - "crm_activity" a - on - (m.subtype_id = a.subtype_id) WHERE (m.model = 'crm.lead') )""") diff --git a/addons/crm/report/crm_opportunity_report.py b/addons/crm/report/crm_opportunity_report.py index c690e79799d..b612d3ad349 100644 --- a/addons/crm/report/crm_opportunity_report.py +++ b/addons/crm/report/crm_opportunity_report.py @@ -68,7 +68,9 @@ class crm_opportunity_report(osv.Model): c.company_id, c.priority, c.team_id, - activity.nbr_activities, + (SELECT COUNT(*) + FROM mail_message m + WHERE m.model = 'crm.lead' and m.res_id = c.id) as nbr_activity, c.active, c.campaign_id, c.source_id, @@ -85,14 +87,7 @@ class crm_opportunity_report(osv.Model): c.date_conversion as date_conversion FROM "crm_lead" c - LEFT JOIN ( - SELECT m.res_id, COUNT(*) nbr_activities - FROM "mail_message" m - WHERE m.model = 'crm.lead' - GROUP BY m.res_id ) activity - ON - (activity.res_id = c.id) LEFT JOIN "crm_stage" stage ON stage.id = c.stage_id - GROUP BY c.id, activity.nbr_activities, stage.name + GROUP BY c.id, stage.name )""") diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index 233723ce712..dd9459b38bf 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -93,11 +93,6 @@ class SaleOrder(models.Model): if self.order_line: values['sequence'] = self.order_line[-1].sequence + 1 sol = SaleOrderLine.create(values) - if hasattr(sol, 'product_id_change_margin'): - # if `sale_margin` module is installed, force computation of field `purchase_price` - # As there is not dependency between modules (nor a bridge module), this is the only - # place we can get the value for this field - sol.product_id_change_margin() return sol diff --git a/addons/payment/security/ir.model.access.csv b/addons/payment/security/ir.model.access.csv index 942718807d5..4b04317deb3 100644 --- a/addons/payment/security/ir.model.access.csv +++ b/addons/payment/security/ir.model.access.csv @@ -6,6 +6,6 @@ payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,0,0 payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0 payment_transaction_system,payment.transaction.system,model_payment_transaction,base.group_system,1,1,1,1 payment_method_all,payment.method.all,model_payment_method,,1,0,0,0 -payment_method_user,payment.method.user,model_payment_method,base.group_user,1,1,1,0 -payment_method_portal,payment.method.portal,model_payment_method,base.group_portal,1,1,1,0 +payment_method_user,payment.method.user,model_payment_method,base.group_user,1,1,1,1 +payment_method_portal,payment.method.portal,model_payment_method,base.group_portal,1,1,1,1 payment_method_system,payment.method.system,model_payment_method,base.group_system,1,1,1,1 diff --git a/addons/pos_mercury/static/src/xml/pos_mercury.xml b/addons/pos_mercury/static/src/xml/pos_mercury.xml index bb8071a5a59..02f25588a3c 100644 --- a/addons/pos_mercury/static/src/xml/pos_mercury.xml +++ b/addons/pos_mercury/static/src/xml/pos_mercury.xml @@ -55,7 +55,6 @@ -
  APPROVAL CODE:
diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 667c3f79c39..6f9be4c82bb 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -318,6 +318,9 @@ class SaleOrder(models.Model): invoice.type = 'out_refund' for line in invoice.invoice_line_ids: line.quantity = -line.quantity + # Use additional field helper function (for account extensions) + for line in invoice.invoice_line_ids: + line._set_additional_fields(invoice) # Necessary to force computation of taxes. In account_invoice, they are triggered # by onchanges, which are not triggered when doing a create. invoice.compute_taxes() @@ -759,15 +762,6 @@ class SaleOrderLine(models.Model): raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.')) return super(SaleOrderLine, self).unlink() - def onchange_product_uom(self, cursor, user, ids, pricelist, product, qty=0, - uom=False, qty_uos=0, uos=False, name='', partner_id=False, - lang=False, update_tax=True, date_order=False, fiscal_position=False, context=None): - ctx = dict(context or {}, fiscal_position=fiscal_position) - return self.product_uom_change(cursor, user, ids, pricelist, product, - qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, - partner_id=partner_id, lang=lang, update_tax=update_tax, - date_order=date_order, context=ctx) - @api.multi def _get_delivered_qty(self): ''' diff --git a/addons/sale_margin/sale_margin.py b/addons/sale_margin/sale_margin.py index 12398dce41e..a585821a32d 100644 --- a/addons/sale_margin/sale_margin.py +++ b/addons/sale_margin/sale_margin.py @@ -7,20 +7,35 @@ import openerp.addons.decimal_precision as dp class sale_order_line(osv.osv): _inherit = "sale.order.line" - @api.multi + def _compute_margin(self, order_id, product_id, product_uom_id): + frm_cur = self.env.user.company_id.currency_id + to_cur = order_id.pricelist_id.currency_id + purchase_price = product_id.standard_price + if product_uom_id != product_id.uom_id: + purchase_price = self.env['product.uom']._compute_price(product_id.uom_id.id, purchase_price, to_uom_id=product_uom_id.id) + ctx = self.env.context.copy() + ctx['date'] = order_id.date_order + price = frm_cur.with_context(ctx).compute(purchase_price, to_cur, round=False) + return price + @api.onchange('product_id', 'product_uom') def product_id_change_margin(self): - for line in self: - if line.order_id.pricelist_id: - frm_cur = self.env.user.company_id.currency_id - to_cur = line.order_id.pricelist_id.currency_id - purchase_price = line.product_id.standard_price - if line.product_uom != line.product_id.uom_id: - purchase_price = self.env['product.uom']._compute_price(line.product_id.uom_id.id, purchase_price, to_uom_id=line.product_uom.id) - ctx = self.env.context.copy() - ctx['date'] = line.order_id.date_order - price = frm_cur.with_context(ctx).compute(purchase_price, to_cur, round=False) - line.purchase_price = price + if not self.order_id.pricelist_id or not self.product_id or not self.product_uom: + return + self.purchase_price = self._compute_margin(self.order_id, self.product_id, self.product_uom) + + @api.model + def create(self, vals): + # Calculation of the margin for programmatic creation of a SO line. It is therefore not + # necessary to call product_id_change_margin manually + if 'purchase_price' not in vals: + order_id = self.env['sale.order'].browse(vals['order_id']) + product_id = self.env['product.product'].browse(vals['product_id']) + product_uom_id = self.env['product.uom'].browse(vals['product_uom']) + + vals['purchase_price'] = self._compute_margin(order_id, product_id, product_uom_id) + + return super(sale_order_line, self).create(vals) def _product_margin(self, cr, uid, ids, field_name, arg, context=None): cur_obj = self.pool.get('res.currency') diff --git a/addons/sale_mrp/tests/test_move_explode.py b/addons/sale_mrp/tests/test_move_explode.py index a7e3bdea946..84a13aff959 100644 --- a/addons/sale_mrp/tests/test_move_explode.py +++ b/addons/sale_mrp/tests/test_move_explode.py @@ -48,7 +48,7 @@ class TestMoveExplode(common.TransactionCase): 'partner_shipping_id': self.partner.id, 'pricelist_id': self.pricelist.id, } - self.so = self.SaleOrder.create(vals=so_vals) + self.so = self.SaleOrder.create(so_vals) sol_vals = { 'order_id': self.so.id, 'name': self.product_bom.name, @@ -56,7 +56,7 @@ class TestMoveExplode(common.TransactionCase): 'product_uom': self.product_bom.uom_id.id, 'product_uom_qty': 1.0, } - self.SaleOrderLine.create(values=sol_vals) + self.SaleOrderLine.create(sol_vals) #confirm sale order self.so.action_confirm() #get all move associated to that sale_order diff --git a/addons/web_kanban/static/src/js/kanban_widgets.js b/addons/web_kanban/static/src/js/kanban_widgets.js index 014e50dec3f..53b75bb3bc1 100644 --- a/addons/web_kanban/static/src/js/kanban_widgets.js +++ b/addons/web_kanban/static/src/js/kanban_widgets.js @@ -54,7 +54,7 @@ var FormatChar = AbstractField.extend({ this.format_descriptor = _.extend({}, this.field, {'widget': this.$node.attr('widget')}); }, renderElement: function() { - this.$el.text(instance.web.format_value(this.field.raw_value, this.format_descriptor)); + this.$el.text(formats.format_value(this.field.raw_value, this.format_descriptor)); } }); diff --git a/addons/web_tip/web_tip_data.xml b/addons/web_tip/web_tip_data.xml index 6102d80637d..db24632ce9e 100644 --- a/addons/web_tip/web_tip_data.xml +++ b/addons/web_tip/web_tip_data.xml @@ -43,8 +43,8 @@ form - button > .fa-archive - button > .fa-archive + [name="button_box"] button > .fa-archive + [name="button_box"] button:has(.fa-archive) mousedown bottom diff --git a/addons/website/models/website.py b/addons/website/models/website.py index c1ca74d7c1d..88ad2afd3ea 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -403,10 +403,13 @@ class website(osv.osv): return Access.check(cr, uid, 'ir.ui.menu', 'read', False, context=context) def get_template(self, cr, uid, ids, template, context=None): - if not isinstance(template, (int, long)) and '.' not in template: - template = 'website.%s' % template View = self.pool['ir.ui.view'] - view_id = View.get_view_id(cr, uid, template, context=context) + if isinstance(template, (int, long)): + view_id = template + else: + if '.' not in template: + template = 'website.%s' % template + view_id = View.get_view_id(cr, uid, template, context=context) if not view_id: raise NotFound return View.browse(cr, uid, view_id, context=context) diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 18311e0ce53..2488726ff48 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -165,7 +165,7 @@ class ir_attachment(osv.osv): fname_to_delete = attach.store_fname location = self._storage(cr, uid, context) # compute the index_content field - vals['index_content'] = self._index(cr, SUPERUSER_ID, bin_data, attach.datas_fname, attach.mimetype), + vals['index_content'] = self._index(cr, SUPERUSER_ID, bin_data, attach.datas_fname, attach.mimetype) if location != 'db': # create the file fname = self._file_write(cr, uid, value, checksum)