diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 872bd8f13d0..3687e602f12 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -983,6 +983,11 @@ class AccountTax(models.Model): if self.amount_type in ('percent', 'division') and self.amount != 0.0 and not self.description: self.description = "{0:.4g}%".format(self.amount) + @api.onchange('amount_type') + def onchange_amount_type(self): + if self.amount_type is not 'group': + self.children_tax_ids = [(5,)] + @api.onchange('account_id') def onchange_account_id(self): self.refund_account_id = self.account_id diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 12c223e89d8..5c5ee899d7c 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -127,7 +127,12 @@ class AccountInvoice(models.Model): def _get_outstanding_info_JSON(self): self.outstanding_credits_debits_widget = json.dumps(False) if self.state == 'open': - domain = [('account_id', '=', self.account_id.id), ('partner_id', '=', self.env['res.partner']._find_accounting_partner(self.partner_id).id), ('reconciled', '=', False), '|', ('amount_residual', '!=', 0.0), ('amount_residual_currency', '!=', 0.0)] + domain = [('account_id', '=', self.account_id.id), + ('partner_id', '=', self.env['res.partner']._find_accounting_partner(self.partner_id).id), + ('reconciled', '=', False), + '|', + '&', ('amount_residual_currency', '!=', 0.0), ('currency_id','!=', None), + '&', ('amount_residual_currency', '=', 0.0), '&', ('currency_id','=', None), ('amount_residual', '!=', 0.0)] if self.type in ('out_invoice', 'in_refund'): domain.extend([('credit', '>', 0), ('debit', '=', 0)]) type_payment = _('Outstanding credits') diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 7dbe669391d..8ba23fda42f 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -508,7 +508,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { } return this.model.makeRecord('account.bank.statement.line', [field], { partner_id: { - domain: [["parent_id", "=", false], "|", ["customer", "=", true], ["supplier", "=", true]], + domain: ["|", ["is_company", "=", true], ["parent_id", "=", false], "|", ["customer", "=", true], ["supplier", "=", true]], options: { no_open: true } diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 82ecfc1d1e0..e1224663214 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -29,6 +29,7 @@ var db = { customer: {string: "customer", type: 'boolean'}, supplier: {string: "supplier", type: 'boolean'}, parent_id: {string: "Parent", type: 'boolean'}, + is_company: {string: "Is company", type: 'boolean'}, property_account_receivable_id: {string: 'Account receivable', type: 'many2one', relation: 'account.account'}, property_account_payable_id: {string: 'Account payable', type: 'many2one', relation: 'account.account'}, }, diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index e519c6a0cec..2965d4dad7a 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1937,7 +1937,7 @@ class MailThread(models.AbstractModel): if False/0, mail.message model will also be set as False :param str body: body of the message, usually raw HTML that will be sanitized - :param str type: see mail_message.type field + :param str type: see mail_message.message_type field :param int parent_id: handle reply to a previous message by adding the parent partners to the message in case of private discussion :param tuple(str,str) attachments or list id: list of attachment tuples in the form diff --git a/addons/sale/models/res_partner.py b/addons/sale/models/res_partner.py index 26039c1f2f6..d03c877b938 100644 --- a/addons/sale/models/res_partner.py +++ b/addons/sale/models/res_partner.py @@ -16,13 +16,10 @@ class ResPartner(models.Model): def _compute_sale_order_count(self): sale_data = self.env['sale.order'].read_group(domain=[('partner_id', 'child_of', self.ids)], fields=['partner_id'], groupby=['partner_id']) - # read to keep the child/parent relation while aggregating the read_group result in the loop - partner_child_ids = self.read(['child_ids']) mapped_data = dict([(m['partner_id'][0], m['partner_id_count']) for m in sale_data]) for partner in self: - # let's obtain the partner id and all its child ids from the read up there - item = next(p for p in partner_child_ids if p['id'] == partner.id) - partner_ids = [partner.id] + item.get('child_ids') + # let's obtain the partner id and all its child ids + partner_ids = [partner.id] + partner.child_ids.ids # then we can sum for all the partner's child partner.sale_order_count = sum(mapped_data.get(child, 0) for child in partner_ids) diff --git a/addons/sale/models/sales_team.py b/addons/sale/models/sales_team.py index bb0686c50c9..edcfe64688e 100644 --- a/addons/sale/models/sales_team.py +++ b/addons/sale/models/sales_team.py @@ -69,7 +69,7 @@ class CrmTeam(models.Model): ('order_line.qty_to_invoice', '>', 0), ], ['team_id'], ['team_id']) for datum in sale_order_data: - self.browse(datum['team_id'][0]).invoiced = datum['team_id_count'] + self.browse(datum['team_id'][0]).sales_to_invoice_count = datum['team_id_count'] @api.multi def _compute_invoiced(self): diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 78666bc2669..421a9ccef61 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -397,7 +397,7 @@ class StockMove(models.Model): # Get the standard price amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price qty = forced_qty or qty_done - new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done) + new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty) tmpl_dict[move.product_id.id] += qty_done # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 4afd3706d5e..701d97e8dd1 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -2621,6 +2621,46 @@ class TestStockValuation(TransactionCase): self.assertAlmostEqual(self.product1.standard_price, 7.5) + def test_average_perpetual_7(self): + """ Test edit in the past. Receive 5@10, receive 10@20, edit the first move to receive + 15 instead. + """ + self.product1.product_tmpl_id.cost_method = 'average' + + move1 = self.env['stock.move'].create({ + 'name': 'IN 5@10', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 5, + 'price_unit': 10, + }) + move1._action_confirm() + move1.quantity_done = 5 + move1._action_done() + + self.assertAlmostEqual(self.product1.standard_price, 10) + + move2 = self.env['stock.move'].create({ + 'name': 'IN 10@20', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 10, + 'price_unit': 20, + }) + move2._action_confirm() + move2.quantity_done = 10 + move2._action_done() + + self.assertAlmostEqual(self.product1.standard_price, 16.67) + + move1.move_line_ids.qty_done = 15 + + self.assertAlmostEqual(self.product1.standard_price, 14.0) + def test_average_negative_1(self): """ Test edit in the past. Receive 10, send 20, edit the second move to only send 10. """ diff --git a/addons/stock_dropshipping/models/purchase.py b/addons/stock_dropshipping/models/purchase.py index afd07a3b212..af98e29d390 100644 --- a/addons/stock_dropshipping/models/purchase.py +++ b/addons/stock_dropshipping/models/purchase.py @@ -14,6 +14,12 @@ class PurchaseOrderLine(models.Model): re['sale_line_id'] = self.sale_line_id.id return res + def _merge_in_existing_line(self, product_id, product_qty, product_uom, location_id, name, origin, values): + if values.get('route_ids') and values['route_ids'] == self.env.ref('stock_dropshipping.route_drop_shipping'): + return False + return super(PurchaseOrderLine, self)._merge_in_existing_line( + product_id=product_id, product_qty=product_qty, product_uom=product_uom, + location_id=location_id, name=name, origin=origin, values=values) class StockRule(models.Model): _inherit = 'stock.rule' diff --git a/addons/web/static/src/js/views/search/search_inputs.js b/addons/web/static/src/js/views/search/search_inputs.js index 43fbea08f9c..d92616cfe3e 100644 --- a/addons/web/static/src/js/views/search/search_inputs.js +++ b/addons/web/static/src/js/views/search/search_inputs.js @@ -347,7 +347,11 @@ var BooleanField = SelectionField.extend(/** @lends instance.web.search.BooleanF var DateField = Field.extend(/** @lends instance.web.search.DateField# */{ value_from: function (facetValue) { var serverFormat = (this.attrs && this.attrs.type === 'datetime') ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; - return facetValue.get('valueAsMoment').clone().format(serverFormat); + if (facetValue.get('valueAsMoment')) { + return facetValue.get('valueAsMoment').clone().format(serverFormat); + } else { + return moment(facetValue.get('value')).format(serverFormat); + } }, complete: function (needle) { // Make sure the needle has a correct format before the creation of the moment object. See diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 770508ac1b8..85b9b9f6f07 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -52,7 +52,6 @@ } &:not(:empty)::after { margin-left: 6px; - position: absolute; } &.o-sort-up { cursor: n-resize; diff --git a/addons/website_event/static/src/js/website_geolocation.js b/addons/website_event/static/src/js/website_geolocation.js index 069f4bcbbf4..5d1f3d26775 100644 --- a/addons/website_event/static/src/js/website_geolocation.js +++ b/addons/website_event/static/src/js/website_geolocation.js @@ -4,7 +4,7 @@ odoo.define('website_event.geolocation', function (require) { var sAnimation = require('website.content.snippets.animation'); sAnimation.registry.visitor = sAnimation.Class.extend({ - selector: '.oe_country_events', + selector: ".oe_country_events, .country_events", /** * @override diff --git a/doc/cla/individual/obayit.md b/doc/cla/individual/obayit.md new file mode 100644 index 00000000000..71ec8b8725a --- /dev/null +++ b/doc/cla/individual/obayit.md @@ -0,0 +1,11 @@ +Sudan, 2019/1/31 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Obay obayit@gmail.com https://github.com/obayit diff --git a/doc/reference/mixins.rst b/doc/reference/mixins.rst index 40a63bbd125..d018064c8c6 100644 --- a/doc/reference/mixins.rst +++ b/doc/reference/mixins.rst @@ -84,7 +84,7 @@ to manage followers on your record: :param str body: body of the message, usually raw HTML that will be sanitized - :param str message_type: see mail_message.type field + :param str message_type: see mail_message.message_type field :param int parent_id: handle reply to a previous message by adding the parent partners to the message in case of private discussion :param list(tuple(str,str)) attachments: list of attachment tuples in the form diff --git a/odoo/addons/base/tests/test_db_cursor.py b/odoo/addons/base/tests/test_db_cursor.py index 77d4c6e8910..839bcff6f89 100644 --- a/odoo/addons/base/tests/test_db_cursor.py +++ b/odoo/addons/base/tests/test_db_cursor.py @@ -4,6 +4,7 @@ import unittest import odoo +from odoo.sql_db import TestCursor from odoo.tests import common from odoo.tools.misc import mute_logger @@ -29,3 +30,84 @@ class TestExecute(unittest.TestCase): cr.execute("SELECT id FROM res_users WHERE id=%s", 1) with self.assertRaises(ValueError): cr.execute("SELECT id FROM res_users WHERE id=%s", '1') + + +class TestTestCursor(common.TransactionCase): + @classmethod + def setUpClass(cls): + super(TestTestCursor, cls).setUpClass() + r = registry() + r.enter_test_mode(r.cursor()) + + @classmethod + def tearDownClass(cls): + r = registry() + r.test_cr.close() + r.leave_test_mode() + super(TestTestCursor, cls).tearDownClass() + + def setUp(self): + super(TestTestCursor, self).setUp() + self.record = self.env['res.partner'].create({'name': 'Foo'}) + + def write(self, record, value): + record.ref = value + + def check(self, record, value): + self.assertEqual(record.read(['ref'])[0]['ref'], value) + + def test_single_cursor(self): + """ Check the behavior of a single test cursor. """ + self.assertIsInstance(self.cr, TestCursor) + self.write(self.record, 'A') + self.cr.commit() + + self.write(self.record, 'B') + self.cr.rollback() + self.check(self.record, 'A') + + self.write(self.record, 'C') + self.cr.rollback() + self.check(self.record, 'A') + + def test_sub_commit(self): + """ Check the behavior of a subcursor that commits. """ + self.assertIsInstance(self.cr, TestCursor) + self.write(self.record, 'A') + self.cr.commit() + + self.write(self.record, 'B') + + # check behavior of a "sub-cursor" that commits + with self.registry.cursor() as cr: + self.assertIsInstance(cr, TestCursor) + record = self.record.with_env(self.env(cr=cr)) + self.check(record, 'B') + self.write(record, 'C') + + self.check(self.record, 'C') + + self.cr.rollback() + self.check(self.record, 'A') + + def test_sub_rollback(self): + """ Check the behavior of a subcursor that rollbacks. """ + self.assertIsInstance(self.cr, TestCursor) + self.write(self.record, 'A') + self.cr.commit() + + self.write(self.record, 'B') + + # check behavior of a "sub-cursor" that rollbacks + with self.assertRaises(ValueError): + with self.registry.cursor() as cr: + self.assertIsInstance(cr, TestCursor) + record = self.record.with_env(self.env(cr=cr)) + self.check(record, 'B') + self.write(record, 'C') + raise ValueError(42) + + self.check(self.record, 'B') + + self.cr.rollback() + self.check(self.record, 'A') diff --git a/odoo/service/server.py b/odoo/service/server.py index 8ecf6143118..abfbe595cb3 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -802,7 +802,7 @@ class Worker(object): if e.args[0] not in [errno.EINTR]: raise - def process_limit(self): + def check_limits(self): # If our parent changed sucide if self.ppid != os.getppid(): _logger.info("Worker (%s) Parent changed", self.pid) @@ -819,6 +819,7 @@ class Worker(object): set_limit_memory_hard() + def set_limits(self): # SIGXCPU (exceeded CPU time) signal handler will raise an exception. r = resource.getrusage(resource.RUSAGE_SELF) cpu_time = r.ru_utime + r.ru_stime @@ -851,17 +852,18 @@ class Worker(object): signal.signal(signal.SIGCHLD, signal.SIG_DFL) signal.set_wakeup_fd(self.wakeup_fd_w) + self.set_limits() + def stop(self): pass def run(self): try: self.start() - while self.alive: - self.process_limit() - self.multi.pipe_ping(self.watchdog_pipe) - self.sleep() - self.process_work() + t = threading.Thread(name="Worker %s (%s) workthread" % (self.__class__.__name__, self.pid), target=self._runloop) + t.daemon = True + t.start() + t.join() _logger.info("Worker (%s) exiting. request_count: %s, registry count: %s.", self.pid, self.request_count, len(odoo.modules.registry.Registry.registries)) @@ -871,6 +873,17 @@ class Worker(object): # should we use 3 to abort everything ? sys.exit(1) + def _runloop(self): + try: + while self.alive: + self.multi.pipe_ping(self.watchdog_pipe) + self.sleep() + self.process_work() + self.check_limits() + except: + _logger.exception("Worker %s (%s) Exception occured, exiting...", self.__class__.__name__, self.pid) + sys.exit(1) + class WorkerHTTP(Worker): """ HTTP Request workers """ def process_request(self, client, addr):