Permalink
Browse files

[MERGE] forward port branch saas-11.3 up to 1e0a92c

  • Loading branch information...
KangOl committed Sep 13, 2018
2 parents 5cfd3d5 + 1e0a92c commit f3fa5a5ef7d2e86a38f988f799a0ea33304d1e14
Showing with 435 additions and 260 deletions.
  1. +7 −0 addons/account/models/account.py
  2. +20 −15 addons/account/models/account_invoice.py
  3. +13 −3 addons/account/static/src/js/reconciliation/reconciliation_model.js
  4. +5 −0 addons/base_address_extended/models/base_address_extended.py
  5. +31 −15 addons/base_geolocalize/models/res_partner.py
  6. +2 −0 addons/calendar/models/calendar.py
  7. +13 −4 addons/calendar/tests/test_calendar_recurrent_event_case2.py
  8. +1 −1 addons/crm/models/crm_lead.py
  9. +2 −2 addons/hr_expense/models/hr_expense.py
  10. +1 −1 addons/l10n_co/models/res_partner.py
  11. +5 −1 addons/mail/models/ir_actions.py
  12. +3 −1 addons/mail/models/mail_thread.py
  13. +27 −8 addons/mail/static/src/js/activity.js
  14. +70 −0 addons/mail/static/tests/chatter_tests.js
  15. +4 −2 addons/membership/models/partner.py
  16. +2 −0 addons/mrp/wizard/change_production_qty.py
  17. +6 −2 addons/payment/models/payment_acquirer.py
  18. +1 −1 addons/point_of_sale/views/pos_config_view.xml
  19. +1 −1 addons/rating/views/rating_view.xml
  20. +1 −1 addons/sale/views/sale_views.xml
  21. +8 −3 addons/sale_stock/models/sale_order.py
  22. +10 −6 addons/sale_timesheet/models/account_invoice.py
  23. +3 −2 addons/stock/models/stock_picking.py
  24. +21 −18 addons/stock/tests/test_packing_neg.py
  25. +9 −12 addons/stock/tests/test_proc_rule.py
  26. +21 −17 addons/stock/tests/test_wise_operator.py
  27. +2 −1 addons/stock_dropshipping/models/__init__.py
  28. +21 −0 addons/stock_dropshipping/models/sale.py
  29. +0 −1 addons/stock_dropshipping/tests/__init__.py
  30. +44 −0 addons/stock_dropshipping/tests/test_dropship.py
  31. +4 −2 addons/web/static/src/js/chrome/abstract_web_client.js
  32. +1 −5 addons/web/static/src/js/views/kanban/kanban_model.js
  33. +1 −2 addons/web/static/src/js/views/list/list_renderer.js
  34. +3 −3 addons/web/static/tests/chrome/action_manager_tests.js
  35. +0 −37 addons/web/static/tests/views/kanban_tests.js
  36. +0 −70 addons/web/static/tests/views/list_tests.js
  37. +1 −0 addons/web_settings_dashboard/static/src/js/dashboard.js
  38. +2 −2 addons/web_settings_dashboard/static/tests/dashboard_tests.js
  39. +4 −13 addons/website_crm_partner_assign/models/crm_lead.py
  40. +1 −1 addons/website_crm_partner_assign/tests/test_partner_assign.py
  41. +4 −3 addons/website_quote/report/sale_order_templates.xml
  42. +0 −1 addons/website_sale/models/sale_order.py
  43. +7 −0 addons/website_sale_stock/models/sale_order.py
  44. +1 −0 doc/cla/corporate/camptocamp.md
  45. +1 −0 doc/cla/corporate/initos.md
  46. +11 −0 doc/cla/individual/ivantodorovich.md
  47. +11 −0 doc/cla/individual/rocketgithub.md
  48. +12 −0 doc/reference/views.rst
  49. +1 −1 odoo/addons/base/data/res_country_data.xml
  50. +1 −1 odoo/addons/base/models/res_partner.py
  51. +14 −1 odoo/addons/test_main_flows/__init__.py
  52. +1 −0 odoo/addons/test_main_flows/__manifest__.py
@@ -284,6 +284,12 @@ def write(self, vals):
self.filtered(lambda r: not r.reconcile)._toggle_reconcile_to_true()
else:
self.filtered(lambda r: r.reconcile)._toggle_reconcile_to_false()
if vals.get('currency_id'):
for account in self:
if self.env['account.move.line'].search_count([('account_id', '=', account.id), ('currency_id', 'not in', (False, vals['currency_id']))]):
raise UserError(_('You cannot set a currency on this account as it already has some journal entries having a different foreign currency.'))
return super(AccountAccount, self).write(vals)
@api.multi
@@ -1086,6 +1092,7 @@ def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, par
'refund_account_id': tax.refund_account_id.id,
'analytic': tax.analytic,
'price_include': tax.price_include,
'tax_exigibility': tax.tax_exigibility,
})
return {
@@ -1461,18 +1461,7 @@ def refund(self, date_invoice=None, date=None, description=None, journal_id=None
new_invoices += refund_invoice
return new_invoices
@api.multi
def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None):
""" Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice.
:param pay_journal: journal in which the payment entry will be created
:param pay_amount: amount of the payment to register, defaults to the residual of the invoice
:param date: payment date, defaults to fields.Date.context_today(self)
:param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid
"""
if isinstance(pay_journal, pycompat.integer_types):
pay_journal = self.env['account.journal'].browse([pay_journal])
assert len(self) == 1, "Can only pay one invoice at a time."
def _prepare_payment_vals(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None, communication=None):
payment_type = self.type in ('out_invoice', 'in_refund') and 'inbound' or 'outbound'
if payment_type == 'inbound':
payment_method = self.env.ref('account.account_payment_method_manual_in')
@@ -1481,9 +1470,10 @@ def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_ac
payment_method = self.env.ref('account.account_payment_method_manual_out')
journal_payment_methods = pay_journal.outbound_payment_method_ids
communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number
if self.origin:
communication = '%s (%s)' % (communication, self.origin)
if not communication:
communication = self.type in ('in_invoice', 'in_refund') and self.reference or self.number
if self.origin:
communication = '%s (%s)' % (communication, self.origin)
payment_vals = {
'invoice_ids': [(6, 0, self.ids)],
@@ -1498,7 +1488,22 @@ def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_ac
'payment_difference_handling': writeoff_acc and 'reconcile' or 'open',
'writeoff_account_id': writeoff_acc and writeoff_acc.id or False,
}
return payment_vals
@api.multi
def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None):
""" Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice.
:param pay_journal: journal in which the payment entry will be created
:param pay_amount: amount of the payment to register, defaults to the residual of the invoice
:param date: payment date, defaults to fields.Date.context_today(self)
:param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid
"""
if isinstance(pay_journal, pycompat.integer_types):
pay_journal = self.env['account.journal'].browse([pay_journal])
assert len(self) == 1, "Can only pay one invoice at a time."
payment_vals = self._prepare_payment_vals(pay_journal, pay_amount=pay_amount, date=date, writeoff_acc=writeoff_acc)
payment = self.env['account.payment'].create(payment_vals)
payment.post()
@@ -779,7 +779,7 @@ var StatementModel = BasicModel.extend({
}).then(function (result) {
if (result.length > 0) {
var line = self.getLine(handle);
self.lines[handle].st_line.open_balance_account_id = line.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0];
self.lines[handle].st_line.open_balance_account_id = line.balance.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0];
}
});
},
@@ -836,6 +836,7 @@ var StatementModel = BasicModel.extend({
'__focus': false
});
prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined
prop.amount = tax.base;
prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
prop.invalid = !self._isValid(prop);
@@ -1129,6 +1130,7 @@ var StatementModel = BasicModel.extend({
name : prop.label,
debit : amount > 0 ? amount : 0,
credit : amount < 0 ? -amount : 0,
tax_exigible: prop.tax_exigible,
analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, 'id')]]
};
if (!isNaN(prop.id)) {
@@ -1191,7 +1193,15 @@ var ManualModel = StatementModel.extend({
self.accounts = _.object(self.account_ids, _.pluck(accounts, 'code'));
});
return def_account.then(function () {
var def_reconcileModel = this._rpc({
model: 'account.reconcile.model',
method: 'search_read',
})
.then(function (reconcileModels) {
self.reconcileModels = reconcileModels;
});
return $.when(def_reconcileModel, def_account).then(function () {
switch(context.mode) {
case 'customers':
case 'suppliers':
@@ -1391,7 +1401,7 @@ var ManualModel = StatementModel.extend({
offset: 0,
limitMoveLines: this.limitMoveLines,
filter: "",
reconcileModels: [],
reconcileModels: this.reconcileModels,
account_id: this._formatNameGet([data.account_id, data.account_name]),
st_line: data,
visible: true
@@ -33,6 +33,11 @@ class Partner(models.Model):
street_number2 = fields.Char('Door', compute='_split_street', help="Door Number",
inverse='_set_street', store=True)
@api.model
def _address_fields(self):
"""Returns the list of address fields that are synced from the parent."""
return super(Partner, self)._address_fields() + ['street_name', 'street_number', 'street_number2']
def get_street_fields(self):
"""Returns the fields that can be used in a street format.
Overwrite this function if you want to add your own fields."""
@@ -1,23 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import requests
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
def geo_find(addr):
def geo_find(addr, apikey=False):
if not addr:
return None
url = 'https://maps.googleapis.com/maps/api/geocode/json'
if not apikey:
raise UserError(_('''API key for GeoCoding (Places) required.\n
Save this key in System Parameters with key: google.api_key_geocode, value: <your api key>
Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information.
'''))
url = "https://maps.googleapis.com/maps/api/geocode/json"
try:
result = requests.get(url, params={'sensor': 'false', 'address': addr}).json()
result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json()
except Exception as e:
raise UserError(_('Cannot contact geolocation servers. Please make sure that your Internet connection is up and running (%s).') % e)
if result['status'] != 'OK':
if result.get('error_message'):
_logger.error(result['error_message'])
return None
try:
@@ -45,22 +57,26 @@ class ResPartner(models.Model):
partner_longitude = fields.Float(string='Geo Longitude', digits=(16, 5))
date_localization = fields.Date(string='Geolocation Date')
@classmethod
def _geo_localize(cls, apikey, street='', zip='', city='', state='', country=''):
search = geo_query_address(street=street, zip=zip, city=city, state=state, country=country)
result = geo_find(search, apikey)
if result is None:
search = geo_query_address(city=city, state=state, country=country)
result = geo_find(search, apikey)
return result
@api.multi
def geo_localize(self):
# We need country names in English below
apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode')
for partner in self.with_context(lang='en_US'):
result = geo_find(geo_query_address(street=partner.street,
zip=partner.zip,
city=partner.city,
state=partner.state_id.name,
country=partner.country_id.name))
if result is None:
result = geo_find(geo_query_address(
city=partner.city,
state=partner.state_id.name,
country=partner.country_id.name
))
result = partner._geo_localize(apikey,
partner.street,
partner.zip,
partner.city,
partner.state_id.name,
partner.country_id.name)
if result:
partner.write({
'partner_latitude': result[0],
@@ -632,6 +632,8 @@ def todate(date):
recurring_date = recurring_date.replace(tzinfo=None)
else:
recurring_date = todate(meeting.recurrent_id_date)
if date_field == "stop":
recurring_date += timedelta(hours=self.duration)
rset1.exdate(recurring_date)
invalidate = True
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR]
@@ -85,14 +85,23 @@ def test_recurrent_meeting3(self):
for meeting in meetings:
self.assertEqual(meeting.name, 'Sprint Review for google modules', 'Name not changed for id: %s' % meeting.id)
# I detach first occurrence to check it is not modified by changing recurrent event.
min(meetings, key=lambda m: m.start).detach_recurring_event()
# I change description of my weekly meeting Review code with programmer.
idval = '%d-%s' % (self.calendar_event_sprint_review.id, '20110425124700')
self.CalendarEvent.browse(idval).write({'description': 'Review code of the module: sync_google_calendar.'})
# I check whether that all the records of this recurrence has been edited.
meetings = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)])
for meeting in meetings:
self.assertEqual(meeting.description, 'Review code of the module: sync_google_calendar.', 'Description not changed for id: %s' % meeting.id)
# I check that detached event has not been edited.
detached_meeting = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)])
self.assertEqual(detached_meeting.description, False, 'Detached event description changed for id: %s' % meeting.id)
# I verify wether I find an event by date range when subsequent to a detached one.
last_meeting = max(meetings, key=lambda m: m.start)
meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([
('start', '<=', last_meeting.stop), ('stop', '>=', last_meeting.start)
])
self.assertEqual(meetings.id, last_meeting.id, 'Last event should be found searching it by date range')
# I update the description of two meetings, and check that both have been updated
self.calendar_event_sprint_review.write({'description': "Some description"})
@@ -938,7 +938,7 @@ def get_empty_list_help(self, help):
email = '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
email_link = "<a href='mailto:%s'>%s</a>" % (email, email)
sub_title = _('or send an email to %s') % (email_link)
return '<p class="o_view_nocontent_smiling_face">%s</p><p>%s</p>' % (help_title, sub_title)
return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title)
@api.multi
def log_meeting(self, meeting_subject, meeting_date, duration):
@@ -147,14 +147,14 @@ def unlink(self):
@api.model
def get_empty_list_help(self, help_message):
if help_message:
if help_message and "oe_view_nocontent_smiling_face" not in help_message:
use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False
if alias_record and alias_record.alias_domain and alias_record.alias_name:
link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % {
'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
}
return '<p class="oe_view_nocontent_smiling_face">%s</p><p>%s</p>' % (
return '<p class="oe_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (
_('Add a new expense,'),
_('or send receipts by email to %s.') % (link),)
return super(HrExpense, self).get_empty_list_help(help_message)
@@ -45,7 +45,7 @@ def _compute_verification_code(self):
except ValueError:
partner.l10n_co_verification_code = False
@api.constrains('vat', 'commercial_partner_country_id', 'l10n_co_document_type')
@api.constrains('vat', 'country_id', 'l10n_co_document_type')
def check_vat(self):
# check_vat is implemented by base_vat which this localization
# doesn't directly depend on. It is however automatically
@@ -48,7 +48,11 @@ def run_action_email(self, action, eval_context=None):
# TDE CLEANME: when going to new api with server action, remove action
if not action.template_id or not self._context.get('active_id'):
return False
action.template_id.send_mail(self._context.get('active_id'), force_send=False, raise_exception=False)
# Clean context from default_type to avoid making attachment
# with wrong values in subsequent operations
cleaned_ctx = dict(self.env.context)
cleaned_ctx.pop('default_type', None)
action.template_id.with_context(cleaned_ctx).send_mail(self._context.get('active_id'), force_send=False, raise_exception=False)
return False
@api.model
@@ -365,7 +365,9 @@ def get_empty_list_help(self, help):
'email_link': email_link
}
}
return "%(static_help)s<p>%(dyn_help)s</p>" % {
# do not add alias two times if it was added previously
if "oe_view_nocontent_alias" not in help:
return "%(static_help)s<p class='oe_view_nocontent_alias'>%(dyn_help)s</p>" % {
'static_help': help,
'dyn_help': _("Create a new %(document)s by sending an email to %(email_link)s") % {
'document': document_name,
Oops, something went wrong.

0 comments on commit f3fa5a5

Please sign in to comment.