diff options
272 files changed, 2477 insertions, 1211 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index d27848ab760..034e06807ff 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,7 @@ recursive-include openerp *.html recursive-include openerp *.jpeg recursive-include openerp *.jpg recursive-include openerp *.js +recursive-include openerp *.less recursive-include openerp *.md recursive-include openerp *.png recursive-include openerp *.po diff --git a/addons/account/__openerp__.py b/addons/account/__openerp__.py index 3abd46bc02f..6a426c6e774 100644 --- a/addons/account/__openerp__.py +++ b/addons/account/__openerp__.py @@ -48,7 +48,6 @@ Processes like maintaining general ledgers are done through the defined Financia for a particular financial year and for preparation of vouchers there is a module named account_voucher. """, 'website': 'https://www.odoo.com/page/billing', - 'images' : ['images/accounts.jpeg','images/bank_statement.jpeg','images/cash_register.jpeg','images/chart_of_accounts.jpeg','images/customer_invoice.jpeg','images/journal_entries.jpeg'], 'depends' : ['base_setup', 'product', 'analytic', 'board', 'edi', 'report'], 'data': [ 'security/account_security.xml', diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index d06f85e5115..630af63708b 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -873,7 +873,7 @@ class account_bank_statement_line(osv.osv): # Unfortunately, that spawns a "no access rights" error ; it shouldn't. def _needaction_domain_get(self, cr, uid, context=None): user = self.pool.get("res.users").browse(cr, uid, uid) - return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_ids', '=', False)] + return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_ids', '=', False), ('account_id', '=', False)] _order = "statement_id desc, sequence" _name = "account.bank.statement.line" diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 83fcebb45ce..b0dbb55bd51 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -2554,7 +2554,7 @@ <field name="sign"/> <field name="style_overwrite"/> </group> - <notebook attrs="{'invisible': [('type','not in',['accounts','account_type'])]}"> + <notebook attrs="{'invisible': [('type','not in',['accounts','account_type', 'account_report'])]}"> <page string="Report"> <group> <field name="display_detail" attrs="{'invisible': [('type','not in',['accounts','account_type'])]}"/> diff --git a/addons/account/static/src/css/account_bank_statement_reconciliation.css b/addons/account/static/src/css/account_bank_statement_reconciliation.css index 3543360fb26..a5710c2e0b1 100644 --- a/addons/account/static/src/css/account_bank_statement_reconciliation.css +++ b/addons/account/static/src/css/account_bank_statement_reconciliation.css @@ -55,6 +55,10 @@ top: 2px; z-index: 10; text-shadow: -1px -1px 0 #f5f5f5, 1px -1px 0 #f5f5f5, -1px 1px 0 #f5f5f5, 1px 1px 0 #f5f5f5; } + .openerp .oe_bank_statement_reconciliation .show_more_container { + text-align: center; } + .openerp .oe_bank_statement_reconciliation .show_more_container .show_more { + display: none; } .openerp .oe_bank_statement_reconciliation .notification_area { margin: 0 15px; } .openerp .oe_bank_statement_reconciliation .interface_options { diff --git a/addons/account/static/src/css/account_bank_statement_reconciliation.scss b/addons/account/static/src/css/account_bank_statement_reconciliation.scss index 1b4acdb2ee5..a773e20b4f4 100644 --- a/addons/account/static/src/css/account_bank_statement_reconciliation.scss +++ b/addons/account/static/src/css/account_bank_statement_reconciliation.scss @@ -92,6 +92,13 @@ $aestetic_animation_speed: 300ms; } } + .show_more_container { + text-align: center; + + .show_more { + display: none; + } + } .notification_area { margin: 0 $actionColWidth; } diff --git a/addons/account/static/src/js/account_widgets.js b/addons/account/static/src/js/account_widgets.js index 126ceaa6d94..136c571f495 100644 --- a/addons/account/static/src/js/account_widgets.js +++ b/addons/account/static/src/js/account_widgets.js @@ -15,11 +15,13 @@ openerp.account = function (instance) { "click .statement_name span": "statementNameClickHandler", "keyup .change_statement_name_field": "changeStatementNameFieldHandler", "click .change_statement_name_button": "changeStatementButtonClickHandler", + "click .show_more": "showMoreButtonClickHandler", }, init: function(parent, context) { this._super(parent); - this.max_reconciliations_displayed = 10; + // Number of reconciliations loaded initially and by clicking 'show more' + this.num_reconciliations_fetched_in_batch = 10; if (context.context.statement_id) this.statement_ids = [context.context.statement_id]; if (context.context.statement_ids) this.statement_ids = context.context.statement_ids; this.single_statement = this.statement_ids !== undefined && this.statement_ids.length === 1; @@ -237,7 +239,7 @@ openerp.account = function (instance) { total_lines: self.already_reconciled_lines+self.st_lines.length })); self.updateProgressbar(); - var reconciliations_to_show = self.st_lines.slice(0, self.max_reconciliations_displayed); + var reconciliations_to_show = self.st_lines.slice(0, self.num_reconciliations_fetched_in_batch); self.last_displayed_reconciliation_index = reconciliations_to_show.length; self.$(".reconciliation_lines_container").css("opacity", 0); @@ -251,6 +253,7 @@ openerp.account = function (instance) { $.when.apply($, child_promises).then(function(){ self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed); self.getChildren()[0].set("mode", "match"); + self.updateShowMoreButton(); if (self.notifications) { self.displayNotifications(self.notifications); } @@ -340,7 +343,7 @@ openerp.account = function (instance) { var child = reconciliations[i]; data.push([child.st_line_id, child.makeMoveLineDicts()]); } - var deferred_animation = self.$(".reconciliation_lines_container").fadeOut(self.aestetic_animation_speed); + var deferred_animation = self.$(".reconciliation_lines_container, .show_more_container").fadeOut(self.aestetic_animation_speed); deferred_rpc = self.model_bank_statement_line.call("process_reconciliations", [data]); return $.when(deferred_animation, deferred_rpc) .done(function() { @@ -359,48 +362,25 @@ openerp.account = function (instance) { // Display new line if there are left if (self.last_displayed_reconciliation_index < self.st_lines.length) { - var begin = self.last_displayed_reconciliation_index; - var end = Math.min((begin+self.max_reconciliations_displayed), self.st_lines.length); - var reconciliations_to_show = self.st_lines.slice(begin, end); - - return self.model_bank_statement_line - .call("get_data_for_reconciliations", [reconciliations_to_show]) - .then(function (data) { - var child_promises = []; - var datum; - while ((datum = data.shift()) !== undefined) { - var context = { - st_line_id: datum.st_line.id, - mode: 'inactive', - animate_entrance: false, - initial_data_provided: true, - st_line: datum.st_line, - reconciliation_proposition: datum.reconciliation_proposition, - }; - var widget = new instance.web.account.bankStatementReconciliationLine(self, context); - child_promises.push(widget.appendTo(self.$(".reconciliation_lines_container"))); + return self.displayReconciliations(self.num_reconciliations_fetched_in_batch).then(function() { + // Put the first line in match mode + if (self.reconciled_lines !== self.st_lines.length) { + var first_child = self.getChildren()[0]; + if (first_child.get("mode") === "inactive") { + first_child.set("mode", "match"); } - self.last_displayed_reconciliation_index += reconciliations_to_show.length; - return $.when.apply($, child_promises).then(function() { - // Put the first line in match mode - if (self.reconciled_lines !== self.st_lines.length) { - var first_child = self.getChildren()[0]; - if (first_child.get("mode") === "inactive") { - first_child.set("mode", "match"); - } - } - self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed); - }); - }); + } + self.$(".reconciliation_lines_container, .show_more_container").fadeIn(self.aestetic_animation_speed); + }); } else if (self.reconciled_lines === self.st_lines.length) { // Congratulate the user if the work is done self.displayDoneMessage(); } else { // Some lines weren't persisted because they were't valid - self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed); + self.$(".reconciliation_lines_container, .show_more_container").fadeIn(self.aestetic_animation_speed); } }).fail(function() { - self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed); + self.$(".reconciliation_lines_container, .show_more_container").fadeIn(self.aestetic_animation_speed); }); }, @@ -433,7 +413,8 @@ openerp.account = function (instance) { // Update children if needed _.each(self.getChildren(), function(child){ - if ((child.partner_id === partner_id || child.st_line.has_no_partner) && child !== source_child) { + if (child === source_child || child.st_line === undefined) return; + if (child.partner_id === partner_id || child.st_line.has_no_partner) { if (contains_lines(child.get("mv_lines_selected"), line_ids)) { child.set("mv_lines_selected", _.filter(child.get("mv_lines_selected"), function(o){ return line_ids.indexOf(o.id) === -1 })); } else if (contains_lines(child.mv_lines_deselected, line_ids)) { @@ -457,6 +438,7 @@ openerp.account = function (instance) { // Update children if needed _.each(self.getChildren(), function(child){ + if (child.st_line === undefined) return; if (child.partner_id === partner_id && child !== source_child && (child.get("mode") === "match" || child.$el.hasClass("no_match"))) child.updateMatches(); if (child.st_line.has_no_partner && child.get("mode") === "match" || child.$el.hasClass("no_match")) @@ -464,6 +446,42 @@ openerp.account = function (instance) { }); }, + displayReconciliations: function(number) { + var self = this; + var begin = self.last_displayed_reconciliation_index; + var end = Math.min((begin+number), self.st_lines.length); + var reconciliations_to_show = self.st_lines.slice(begin, end); + + // Get ids of selected move lines (to exclude them from reconciliation proposition) + var excluded_move_lines_ids = []; + _.each(self.excluded_move_lines_ids, function(o){ + excluded_move_lines_ids = excluded_move_lines_ids.concat(o); + }); + + return self.model_bank_statement_line + .call("get_data_for_reconciliations", [reconciliations_to_show, excluded_move_lines_ids]) + .then(function (data) { + var child_promises = []; + var datum; + while ((datum = data.shift()) !== undefined) { + var context = { + st_line_id: datum.st_line.id, + mode: 'inactive', + animate_entrance: false, + initial_data_provided: true, + st_line: datum.st_line, + reconciliation_proposition: datum.reconciliation_proposition, + }; + var widget = new instance.web.account.bankStatementReconciliationLine(self, context); + child_promises.push(widget.appendTo(self.$(".reconciliation_lines_container"))); + } + self.last_displayed_reconciliation_index += reconciliations_to_show.length; + return $.when.apply($, child_promises).then(function(){ + self.updateShowMoreButton(); + }); + }); + }, + displayReconciliation: function(st_line_id, mode, animate_entrance, initial_data_provided, st_line, reconciliation_proposition) { var self = this; animate_entrance = (animate_entrance === undefined ? true : animate_entrance); @@ -489,7 +507,7 @@ openerp.account = function (instance) { self.doReloadMenuReconciliation(); // Display new line if there are left - if (self.last_displayed_reconciliation_index < self.st_lines.length) { + if (self.last_displayed_reconciliation_index < self.st_lines.length && self.getChildren().length < self.num_reconciliations_fetched_in_batch) { self.displayReconciliation(self.st_lines[self.last_displayed_reconciliation_index++], 'inactive'); } // Congratulate the user if the work is done @@ -504,6 +522,7 @@ openerp.account = function (instance) { first_child.set("mode", "match"); } } + self.updateShowMoreButton(); }, goBackToStatementsTreeView: function() { @@ -557,6 +576,7 @@ openerp.account = function (instance) { // Render it self.$(".protip").hide(); + self.updateShowMoreButton(); self.$(".oe_form_sheet").append(QWeb.render("bank_statement_reconciliation_done_message", { title: title, time_taken: time_taken, @@ -606,7 +626,19 @@ openerp.account = function (instance) { }); } }, + + showMoreButtonClickHandler: function() { + this.displayReconciliations(this.num_reconciliations_fetched_in_batch); + }, + updateShowMoreButton: function() { + var items_remaining = this.st_lines.length - this.last_displayed_reconciliation_index; + if (items_remaining > 0) + this.$(".show_more").show().find(".num_items_remaining").text(items_remaining); + else + this.$(".show_more").hide(); + }, + updateProgressbar: function() { var self = this; var done = self.already_reconciled_lines + self.reconciled_lines; diff --git a/addons/account/static/src/xml/account_bank_statement_reconciliation.xml b/addons/account/static/src/xml/account_bank_statement_reconciliation.xml index 415be5dde94..9d3ec4a67a4 100644 --- a/addons/account/static/src/xml/account_bank_statement_reconciliation.xml +++ b/addons/account/static/src/xml/account_bank_statement_reconciliation.xml @@ -34,6 +34,7 @@ <div class="oe_clear"></div> <div class="notification_area"></div> <div class="reconciliation_lines_container"></div> + <div class="show_more_container"><button class="show_more">Show more... (<span class="num_items_remaining"></span> remaining)</button></div> <p class='protip'>Tip : Hit ctrl-enter to validate the whole sheet.</p> </div></div> </t> diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index e09bcc6ee99..8a0a9394c3a 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -1,9 +1,3 @@ from . import test_tax from . import test_search from . import test_reconciliation - -fast_suite = [ - test_tax, - test_search, - test_reconciliation, -] diff --git a/addons/account_analytic_analysis/__openerp__.py b/addons/account_analytic_analysis/__openerp__.py index 3e376c526ca..6fe9d954345 100644 --- a/addons/account_analytic_analysis/__openerp__.py +++ b/addons/account_analytic_analysis/__openerp__.py @@ -32,7 +32,6 @@ Adds menu to show relevant information to each manager.You can also view the rep """, 'author': 'Camptocamp / Odoo', 'website': 'https://www.odoo.com/page/billing', - 'images': ['images/bill_tasks_works.jpeg','images/overpassed_accounts.jpeg'], 'depends': ['hr_timesheet_invoice', 'sale'], #although sale is technically not required to install this module, all menuitems are located under 'Sales' application 'data': [ 'security/ir.model.access.csv', diff --git a/addons/account_analytic_default/__openerp__.py b/addons/account_analytic_default/__openerp__.py index 74ae986d163..6aa65c4a6c4 100644 --- a/addons/account_analytic_default/__openerp__.py +++ b/addons/account_analytic_default/__openerp__.py @@ -37,7 +37,6 @@ Allows to automatically select analytic accounts based on criterions: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/accounting', - 'images': ['images/analytic_defaults.jpeg'], 'depends': ['sale_stock'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/account_analytic_plans/__openerp__.py b/addons/account_analytic_plans/__openerp__.py index d4c99aa8fd3..9b5e467e69f 100644 --- a/addons/account_analytic_plans/__openerp__.py +++ b/addons/account_analytic_plans/__openerp__.py @@ -64,7 +64,6 @@ The analytic plan validates the minimum and maximum percentage at the time of cr """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/accounting', - 'images': ['images/analytic_plan.jpeg'], 'depends': ['account', 'account_analytic_default'], 'data': [ 'security/account_analytic_plan_security.xml', diff --git a/addons/account_anglo_saxon/__openerp__.py b/addons/account_anglo_saxon/__openerp__.py index 89c7768ce6b..ee4f7437785 100644 --- a/addons/account_anglo_saxon/__openerp__.py +++ b/addons/account_anglo_saxon/__openerp__.py @@ -39,7 +39,6 @@ account when the invoice is created to transfer this amount to the debtor or creditor account. Secondly, price differences between actual purchase price and fixed product standard price are booked on a separate account.""", - 'images': ['images/account_anglo_saxon.jpeg'], 'depends': ['product', 'purchase'], 'category': 'Accounting & Finance', 'demo': [], diff --git a/addons/account_budget/__openerp__.py b/addons/account_budget/__openerp__.py index 116e84fc960..352914eaa21 100644 --- a/addons/account_budget/__openerp__.py +++ b/addons/account_budget/__openerp__.py @@ -48,7 +48,6 @@ Three reports are available: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/accounting', - 'images': ['images/budget.jpeg','images/budgetary_position.jpeg'], 'depends': ['account'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/account_cancel/__openerp__.py b/addons/account_cancel/__openerp__.py index f27901c6c55..7af5d2bd7f0 100644 --- a/addons/account_cancel/__openerp__.py +++ b/addons/account_cancel/__openerp__.py @@ -32,7 +32,6 @@ This module adds 'Allow Canceling Entries' field on form view of account journal If set to true it allows user to cancel entries & invoices. """, 'website': 'https://www.odoo.com/page/accounting', - 'images': ['images/account_cancel.jpeg'], 'depends' : ['account'], 'data': ['account_cancel_view.xml' ], 'demo': [], diff --git a/addons/account_followup/__openerp__.py b/addons/account_followup/__openerp__.py index fa5d97a3a46..c612d8f6b63 100644 --- a/addons/account_followup/__openerp__.py +++ b/addons/account_followup/__openerp__.py @@ -45,7 +45,6 @@ Note that if you want to check the follow-up level for a given partner/account e """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/billing', - 'images': ['images/follow_ups.jpeg','images/send_followups.jpeg'], 'depends': ['account_accountant', 'mail'], 'data': [ 'security/account_followup_security.xml', diff --git a/addons/account_followup/tests/__init__.py b/addons/account_followup/tests/__init__.py index abad074cc09..84a004e7a11 100644 --- a/addons/account_followup/tests/__init__.py +++ b/addons/account_followup/tests/__init__.py @@ -20,6 +20,3 @@ ############################################################################## from . import test_account_followup -checks = [ - test_account_followup, -] diff --git a/addons/account_payment/__openerp__.py b/addons/account_payment/__openerp__.py index 83940178180..6c04b162e33 100644 --- a/addons/account_payment/__openerp__.py +++ b/addons/account_payment/__openerp__.py @@ -42,7 +42,6 @@ when you get the confirmation from your bank that your order has been accepted that you can book it in your accounting. To help you with that operation, you have a new option to import payment orders as bank statement lines. """, - 'images': ['images/payment_mode.jpeg','images/payment_order.jpeg'], 'depends': ['account','account_voucher'], 'data': [ 'security/account_payment_security.xml', diff --git a/addons/account_sequence/__openerp__.py b/addons/account_sequence/__openerp__.py index 8f12e4850b5..409fb111825 100644 --- a/addons/account_sequence/__openerp__.py +++ b/addons/account_sequence/__openerp__.py @@ -39,7 +39,6 @@ You can customize the following attributes of the sequence: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com', - 'images': ['images/internal_sequence_number.jpeg'], 'depends': ['account'], 'data': [ 'account_sequence_data.xml', diff --git a/addons/account_voucher/__openerp__.py b/addons/account_voucher/__openerp__.py index 5dcfb7f28a6..09279f5eea3 100644 --- a/addons/account_voucher/__openerp__.py +++ b/addons/account_voucher/__openerp__.py @@ -42,7 +42,6 @@ This module manages: 'category': 'Accounting & Finance', 'sequence': 4, 'website' : 'https://www.odoo.com/page/billing', - 'images' : ['images/customer_payment.jpeg','images/journal_voucher.jpeg','images/sales_receipt.jpeg','images/supplier_voucher.jpeg','images/customer_invoice.jpeg','images/customer_refunds.jpeg'], 'depends' : ['account'], 'demo' : [], 'data' : [ diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml index 353f725e52b..2ec6464d418 100644 --- a/addons/account_voucher/account_voucher_view.xml +++ b/addons/account_voucher/account_voucher_view.xml @@ -19,6 +19,24 @@ </tree> </field> </record> + <record model="ir.ui.view" id="view_voucher_tree_nocreate"> + <field name="name">account.voucher.tree_nocreate</field> + <field name="model">account.voucher</field> + <field name="arch" type="xml"> + <tree colors="blue:state == 'draft';gray:state == 'cancel';red:audit" string="Voucher Entries" create="0" edit="0"> + <field name="date"/> + <field name="number"/> + <field name="reference"/> + <field name="partner_id"/> + <field name="journal_id" groups="account.group_account_user"/> + <field name="period_id" invisible="context.get('visible', True)"/> + <field name="type" invisible="context.get('visible', True)"/> + <field name="amount" sum="Total Amount"/> + <field name="audit" invisible="1"/> + <field name="state"/> + </tree> + </field> + </record> <record model="ir.ui.view" id="view_voucher_line_form"> <field name="name">account.voucher.line.form</field> <field name="model">account.voucher.line</field> @@ -40,7 +58,7 @@ <field name="name">account.voucher.form</field> <field name="model">account.voucher</field> <field name="arch" type="xml"> - <form string="Accounting Voucher"> + <form string="Accounting Voucher" create="0" edit="0"> <header> <button name="proforma_voucher" string="Post" states="draft" class="oe_highlight"/> <button name="cancel_voucher" string="Cancel Voucher" type="object" states="posted" confirm="Are you sure you want to unreconcile this record?"/> @@ -160,12 +178,13 @@ <field name="view_type">form</field> <field name="view_mode">tree,form,graph</field> <field name="context">{}</field> - <field name="view_id" eval="view_voucher_tree"/> + <field name="view_id" eval="view_voucher_tree_nocreate"/> <field name="search_view_id" ref="view_voucher_filter"/> </record> <record id="action_journal_voucher_tree" model="ir.actions.act_window.view"> <field eval="1" name="sequence"/> <field name="view_mode">tree</field> + <field name="view_id" ref="view_voucher_tree_nocreate"/> <field name="act_window_id" ref="action_voucher_list"/> </record> <record id="action_journal_voucher_form" model="ir.actions.act_window.view"> diff --git a/addons/account_voucher/voucher_payment_receipt_view.xml b/addons/account_voucher/voucher_payment_receipt_view.xml index 8024942730e..e6c564177cb 100644 --- a/addons/account_voucher/voucher_payment_receipt_view.xml +++ b/addons/account_voucher/voucher_payment_receipt_view.xml @@ -396,7 +396,7 @@ </page> </notebook> <footer> - <button name="button_proforma_voucher" string="Pay" class="oe_highlight" type="object"/> or + <button name="button_proforma_voucher" string="Register Payment" class="oe_highlight" type="object"/> or <button string="Cancel" class="oe_link" special="cancel"/> </footer> </form> diff --git a/addons/analytic_user_function/__openerp__.py b/addons/analytic_user_function/__openerp__.py index d8fa800335c..630eb9ee6dc 100644 --- a/addons/analytic_user_function/__openerp__.py +++ b/addons/analytic_user_function/__openerp__.py @@ -39,7 +39,6 @@ compatible with older configurations. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': ['images/analytic_user_function.jpeg'], 'depends': ['hr_timesheet_sheet'], 'data': ['analytic_user_function_view.xml', 'security/ir.model.access.csv'], 'demo': [], diff --git a/addons/anonymization/__openerp__.py b/addons/anonymization/__openerp__.py index 4415781dd94..89306819bcd 100644 --- a/addons/anonymization/__openerp__.py +++ b/addons/anonymization/__openerp__.py @@ -48,5 +48,4 @@ anonymization process to recover your previous data. ], 'installable': True, 'auto_install': False, - 'images': ['images/anonymization1.jpeg','images/anonymization2.jpeg','images/anonymization3.jpeg'], } diff --git a/addons/association/__openerp__.py b/addons/association/__openerp__.py index 7ed4c0d11f2..84b45367bf3 100644 --- a/addons/association/__openerp__.py +++ b/addons/association/__openerp__.py @@ -37,6 +37,5 @@ membership products (schemes). 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/association1.jpeg'], 'website': 'https://www.odoo.com' } diff --git a/addons/auth_ldap/__openerp__.py b/addons/auth_ldap/__openerp__.py index 5373db4f475..e709ab96995 100644 --- a/addons/auth_ldap/__openerp__.py +++ b/addons/auth_ldap/__openerp__.py @@ -22,7 +22,6 @@ 'name' : 'Authentication via LDAP', 'version' : '1.0', 'depends' : ['base'], - 'images' : ['images/ldap_configuration.jpeg'], 'author' : 'OpenERP SA', #'description': < auto-loaded from README file 'website' : 'https://www.odoo.com', diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 55068bf2742..a5c3a3423e5 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -70,9 +70,9 @@ class OAuthLogin(Home): return providers def get_state(self, provider): - redirect = request.params.get('redirect', 'web') + redirect = request.params.get('redirect') or 'web' if not redirect.startswith(('//', 'http://', 'https://')): - redirect = '%s%s' % (request.httprequest.url_root, redirect) + redirect = '%s%s' % (request.httprequest.url_root, redirect[1:] if redirect[0] == '/' else redirect) state = dict( d=request.session.db, p=provider['id'], diff --git a/addons/base_action_rule/__openerp__.py b/addons/base_action_rule/__openerp__.py index f824468c52e..53857cf014b 100644 --- a/addons/base_action_rule/__openerp__.py +++ b/addons/base_action_rule/__openerp__.py @@ -44,5 +44,4 @@ trigger an automatic reminder email. 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/base_action_rule1.jpeg','images/base_action_rule2.jpeg','images/base_action_rule3.jpeg'], } diff --git a/addons/base_action_rule/tests/__init__.py b/addons/base_action_rule/tests/__init__.py index 7e6a3e21939..7090de46a47 100644 --- a/addons/base_action_rule/tests/__init__.py +++ b/addons/base_action_rule/tests/__init__.py @@ -20,6 +20,3 @@ ############################################################################## from . import base_action_rule_test -checks = [ - base_action_rule_test, -] diff --git a/addons/base_geolocalize/__openerp__.py b/addons/base_geolocalize/__openerp__.py index ac2ac9b9e81..fe8e7b99e17 100644 --- a/addons/base_geolocalize/__openerp__.py +++ b/addons/base_geolocalize/__openerp__.py @@ -38,5 +38,4 @@ Partners geolocalization 'test': [], 'installable': True, 'auto_install': False, - 'images': [], } diff --git a/addons/base_iban/__openerp__.py b/addons/base_iban/__openerp__.py index acf0081d58c..b7196edbded 100644 --- a/addons/base_iban/__openerp__.py +++ b/addons/base_iban/__openerp__.py @@ -35,5 +35,4 @@ with a single statement. 'data': ['base_iban_data.xml' , 'base_iban_view.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/base_iban1.jpeg'], } diff --git a/addons/base_import/tests/__init__.py b/addons/base_import/tests/__init__.py index abef28c8ca1..54ecbe9baab 100644 --- a/addons/base_import/tests/__init__.py +++ b/addons/base_import/tests/__init__.py @@ -1,3 +1 @@ from . import test_cases - -checks = [test_cases] diff --git a/addons/base_setup/__openerp__.py b/addons/base_setup/__openerp__.py index c8756384054..1276a4eff9c 100644 --- a/addons/base_setup/__openerp__.py +++ b/addons/base_setup/__openerp__.py @@ -44,5 +44,4 @@ Shows you a list of applications features to install from. 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/base_setup1.jpeg','images/base_setup2.jpeg','images/base_setup3.jpeg','images/base_setup4.jpeg',], } diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py index fd075e4330c..7742f19d77e 100644 --- a/addons/base_setup/res_config.py +++ b/addons/base_setup/res_config.py @@ -32,7 +32,7 @@ class base_config_settings(osv.osv_memory): help='Work in multi-company environments, with appropriate security access between companies.\n' '-This installs the module multi_company.'), 'module_share': fields.boolean('Allow documents sharing', - help="""Share or embbed any screen of openerp."""), + help="""Share or embbed any screen of Odoo."""), 'module_portal': fields.boolean('Activate the customer portal', help="""Give your customers access to their documents."""), 'module_auth_oauth': fields.boolean('Use external authentication providers, sign in with google, facebook, ...'), diff --git a/addons/base_vat/__openerp__.py b/addons/base_vat/__openerp__.py index 8f9f0c8784d..076eef77638 100644 --- a/addons/base_vat/__openerp__.py +++ b/addons/base_vat/__openerp__.py @@ -58,5 +58,4 @@ only the country code will be validated. 'data': ['base_vat_view.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/1_partner_vat.jpeg'], } diff --git a/addons/board/__openerp__.py b/addons/board/__openerp__.py index f7b7d47a5ff..d6a98f62e76 100644 --- a/addons/board/__openerp__.py +++ b/addons/board/__openerp__.py @@ -41,5 +41,4 @@ Allows users to create custom dashboard. 'qweb': ['static/src/xml/*.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/1_dashboard_definition.jpeg','images/2_publish_note.jpeg','images/3_admin_dashboard.jpeg',], } diff --git a/addons/calendar/__openerp__.py b/addons/calendar/__openerp__.py index 74dab47d99d..16ef09dd103 100644 --- a/addons/calendar/__openerp__.py +++ b/addons/calendar/__openerp__.py @@ -55,5 +55,4 @@ If you need to manage your meetings, you should install the CRM module. 'installable': True, 'application': True, 'auto_install': False, - 'images': ['images/calendar1.jpeg', 'images/calendar2.jpeg', 'images/calendar3.jpeg', 'images/calendar4.jpeg'], } diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index e7c784d3865..6d950e32541 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -852,10 +852,11 @@ class calendar_event(osv.Model): if values.get('start_datetime') or values.get('start_date') or values.get('start') \ or values.get('stop_datetime') or values.get('stop_date') or values.get('stop'): allday = values.get("allday", None) + event = self.browse(cr, uid, id, context=context) if allday is None: if id: - allday = self.read(cr, uid, [id], ['allday'], context=context)[0].get('allday') + allday = event.allday else: allday = False _logger.debug("Calendar - All day is not specified, arbitrarily set to False") @@ -872,10 +873,16 @@ class calendar_event(osv.Model): values[fld] = values['%s_%s' % (fld, key)] diff = False - if allday and values.get('stop_date') and values.get('start_date'): - diff = datetime.strptime(values['stop_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - datetime.strptime(values['start_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - elif values.get('stop_datetime') and values.get('start_datetime'): - diff = datetime.strptime(values['stop_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(values['start_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) + if allday and (values.get('stop_date') or values.get('start_date')): + stop_date = values.get('stop_date') or event.stop_date + start_date = values.get('start_date') or event.start_date + if stop_date and start_date: + diff = datetime.strptime(stop_date.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - datetime.strptime(start_date.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) + elif values.get('stop_datetime') or values.get('start_datetime'): + stop_datetime = values.get('stop_datetime') or event.stop_datetime + start_datetime = values.get('start_datetime') or event.start_datetime + if stop_datetime and start_datetime: + diff = datetime.strptime(stop_datetime.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(start_datetime.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) if diff: duration = float(diff.days) * 24 + (float(diff.seconds) / 3600) values['duration'] = round(duration, 2) @@ -965,12 +972,14 @@ class calendar_event(osv.Model): def _check_closing_date(self, cr, uid, ids, context=None): for event in self.browse(cr, uid, ids, context=context): - if event.stop < event.start: + if event.start_datetime and event.stop_datetime < event.start_datetime: + return False + if event.start_date and event.stop_date < event.start_date: return False return True _constraints = [ - (_check_closing_date, 'Error ! End date cannot be set before start date.', ['start', 'stop']) + (_check_closing_date, 'Error ! End date cannot be set before start date.', ['start_datetime', 'stop_datetime', 'start_date', 'stop_date']) ] def onchange_allday(self, cr, uid, ids, start=False, end=False, starttime=False, endtime=False, startdatetime=False, enddatetime=False, checkallday=False, context=None): diff --git a/addons/claim_from_delivery/__openerp__.py b/addons/claim_from_delivery/__openerp__.py index cfbe1e69bc1..270d5479e31 100644 --- a/addons/claim_from_delivery/__openerp__.py +++ b/addons/claim_from_delivery/__openerp__.py @@ -35,5 +35,4 @@ Adds a Claim link to the delivery order. 'claim_delivery_data.xml',], 'auto_install': False, 'installable': True, - 'images': ['images/1_claim_link_delivery_order.jpeg'], } diff --git a/addons/contacts/__openerp__.py b/addons/contacts/__openerp__.py index 2163fba3949..57733aa8661 100644 --- a/addons/contacts/__openerp__.py +++ b/addons/contacts/__openerp__.py @@ -36,7 +36,6 @@ You can track your suppliers, customers and other contacts. 'data': [ 'contacts_view.xml', ], - 'images': ['images/contacts.jpeg'], 'installable': True, 'application': True, 'auto_install': True, diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index 737551cb424..b91c8e18759 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -119,15 +119,4 @@ Dashboard for CRM will include: 'installable': True, 'application': True, 'auto_install': False, - 'images': [ - 'images/customers.png', - 'images/leads.png', - 'images/opportunities_kanban.png', - 'images/opportunities_form.png', - 'images/opportunities_calendar.png', - 'images/opportunities_graph.png', - 'images/logged_calls.png', - 'images/scheduled_calls.png', - 'images/stages.png', - ], } diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index 49e65151612..ac06e41b1c4 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -113,10 +113,11 @@ class crm_lead2opportunity_partner(osv.osv_memory): res = False lead_ids = vals.get('lead_ids', []) team_id = vals.get('team_id', False) + partner_id = vals.get('partner_id') data = self.browse(cr, uid, ids, context=context)[0] leads = lead.browse(cr, uid, lead_ids, context=context) for lead_id in leads: - partner_id = self._create_partner(cr, uid, lead_id.id, data.action, lead_id.partner_id.id, context=context) + partner_id = self._create_partner(cr, uid, lead_id.id, data.action, partner_id or lead_id.partner_id.id, context=context) res = lead.convert_opportunity(cr, uid, [lead_id.id], partner_id, [], False, context=context) user_ids = vals.get('user_ids', False) if context.get('no_force_assignation'): @@ -139,18 +140,26 @@ class crm_lead2opportunity_partner(osv.osv_memory): w = self.browse(cr, uid, ids, context=context)[0] opp_ids = [o.id for o in w.opportunity_ids] + vals = { + 'team_id': w.team_id.id, + } + if w.partner_id: + vals['partner_id'] = w.partner_id.id if w.name == 'merge': lead_id = lead_obj.merge_opportunity(cr, uid, opp_ids, context=context) lead_ids = [lead_id] lead = lead_obj.read(cr, uid, lead_id, ['type', 'user_id'], context=context) if lead['type'] == "lead": context = dict(context, active_ids=lead_ids) - self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'team_id': w.team_id.id}, context=context) + vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]}) + self._convert_opportunity(cr, uid, ids, vals, context=context) elif not context.get('no_force_assignation') or not lead['user_id']: - lead_obj.write(cr, uid, lead_id, {'user_id': w.user_id.id, 'team_id': w.team_id.id}, context=context) + vals.update({'user_id': w.user_id.id}) + lead_obj.write(cr, uid, lead_id, vals, context=context) else: lead_ids = context.get('active_ids', []) - self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'team_id': w.team_id.id}, context=context) + vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]}) + self._convert_opportunity(cr, uid, ids, vals, context=context) return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context) diff --git a/addons/crm_claim/__openerp__.py b/addons/crm_claim/__openerp__.py index 79f8470423b..d88f8756320 100644 --- a/addons/crm_claim/__openerp__.py +++ b/addons/crm_claim/__openerp__.py @@ -51,9 +51,4 @@ automatically new claims based on incoming emails. ], 'installable': True, 'auto_install': False, - 'images': [ - 'images/claim_categories.jpeg', - 'images/claim_stages.jpeg', - 'images/claims.jpeg' - ], } diff --git a/addons/crm_helpdesk/__openerp__.py b/addons/crm_helpdesk/__openerp__.py index be2ee411f2a..1f0605a36ae 100644 --- a/addons/crm_helpdesk/__openerp__.py +++ b/addons/crm_helpdesk/__openerp__.py @@ -47,5 +47,4 @@ and categorize your interventions with a channel and a priority level. 'test': ['test/process/help-desk.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/helpdesk_analysis.jpeg','images/helpdesk_categories.jpeg','images/helpdesk_requests.jpeg'], } diff --git a/addons/crm_helpdesk/crm_helpdesk_view.xml b/addons/crm_helpdesk/crm_helpdesk_view.xml index a15c4f8ade8..e9d223e932b 100644 --- a/addons/crm_helpdesk/crm_helpdesk_view.xml +++ b/addons/crm_helpdesk/crm_helpdesk_view.xml @@ -105,8 +105,8 @@ </notebook> </sheet> <div class="oe_chatter"> - <field name="message_ids" widget="mail_thread"/> <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> </div> </form> </field> diff --git a/addons/crm_partner_assign/__openerp__.py b/addons/crm_partner_assign/__openerp__.py index 376107ec76f..97d28164184 100644 --- a/addons/crm_partner_assign/__openerp__.py +++ b/addons/crm_partner_assign/__openerp__.py @@ -54,5 +54,4 @@ to an appropriate local partner, based on the distance and the weight that was a 'test': ['test/partner_assign.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/partner_geo_localization.jpeg','images/partner_grade.jpeg'], } diff --git a/addons/decimal_precision/__openerp__.py b/addons/decimal_precision/__openerp__.py index 817576347c6..851684064ed 100644 --- a/addons/decimal_precision/__openerp__.py +++ b/addons/decimal_precision/__openerp__.py @@ -37,5 +37,4 @@ The decimal precision is configured per company. ], 'demo': [], 'installable': True, - 'images': ['images/1_decimal_accuracy_form.jpeg','images/1_decimal_accuracy_list.jpeg'], } diff --git a/addons/decimal_precision/tests/__init__.py b/addons/decimal_precision/tests/__init__.py index feee24c8f15..b9343853f75 100644 --- a/addons/decimal_precision/tests/__init__.py +++ b/addons/decimal_precision/tests/__init__.py @@ -1,6 +1,2 @@ # -*- coding: utf-8 -*- -import test_qweb_float - -checks = [ - test_qweb_float -] +from . import test_qweb_float diff --git a/addons/delivery/__openerp__.py b/addons/delivery/__openerp__.py index 710a2193608..1d1c59652c4 100644 --- a/addons/delivery/__openerp__.py +++ b/addons/delivery/__openerp__.py @@ -44,5 +44,4 @@ invoices from picking, OpenERP is able to add and compute the shipping line. 'test': ['test/delivery_cost.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/1_delivery_method.jpeg','images/2_delivery_pricelist.jpeg'], } diff --git a/addons/delivery/stock.py b/addons/delivery/stock.py index 2ccb1c41915..3f28e553a29 100644 --- a/addons/delivery/stock.py +++ b/addons/delivery/stock.py @@ -54,7 +54,7 @@ class stock_picking(osv.osv): _columns = { 'carrier_id':fields.many2one("delivery.carrier","Carrier"), - 'volume': fields.float('Volume'), + 'volume': fields.float('Volume', copy=False), 'weight': fields.function(_cal_weight, type='float', string='Weight', digits_compute= dp.get_precision('Stock Weight'), multi='_cal_weight', store={ 'stock.picking': (lambda self, cr, uid, ids, c={}: ids, ['move_lines'], 40), @@ -65,8 +65,8 @@ class stock_picking(osv.osv): 'stock.picking': (lambda self, cr, uid, ids, c={}: ids, ['move_lines'], 40), 'stock.move': (_get_picking_line, ['picking_id', 'product_id','product_uom_qty','product_uom'], 40), }), - 'carrier_tracking_ref': fields.char('Carrier Tracking Ref'), - 'number_of_packages': fields.integer('Number of Packages'), + 'carrier_tracking_ref': fields.char('Carrier Tracking Ref', copy=False), + 'number_of_packages': fields.integer('Number of Packages', copy=False), 'weight_uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True,readonly="1",help="Unit of measurement for Weight",), } diff --git a/addons/document/__openerp__.py b/addons/document/__openerp__.py index 25c82d3c84c..7a413f2423e 100644 --- a/addons/document/__openerp__.py +++ b/addons/document/__openerp__.py @@ -51,5 +51,4 @@ This is a complete document management system. 'test': ['test/document_test2.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/1_directories.jpeg','images/2_storage_media.jpeg','images/3_directories_structure.jpeg'], } diff --git a/addons/event/__openerp__.py b/addons/event/__openerp__.py index 029933c1755..7ac32912056 100644 --- a/addons/event/__openerp__.py +++ b/addons/event/__openerp__.py @@ -37,5 +37,4 @@ Key Features ], 'installable': True, 'auto_install': False, - 'images': ['images/1_event_type_list.jpeg', 'images/2_events.jpeg', 'images/3_registrations.jpeg', 'images/events_kanban.jpeg'], } diff --git a/addons/fetchmail/__openerp__.py b/addons/fetchmail/__openerp__.py index be6a6a09a38..1c6ee8bf762 100644 --- a/addons/fetchmail/__openerp__.py +++ b/addons/fetchmail/__openerp__.py @@ -64,5 +64,4 @@ For more specific needs, you may also assign custom-defined actions 'demo': [], 'installable': True, 'auto_install': True, - 'images': ['images/1_email_servers.jpeg'], } diff --git a/addons/fleet/__openerp__.py b/addons/fleet/__openerp__.py index e2cc6595d46..dbe93618ee1 100644 --- a/addons/fleet/__openerp__.py +++ b/addons/fleet/__openerp__.py @@ -29,7 +29,7 @@ 'description' : """ Vehicle, leasing, insurances, cost ================================== -With this module, OpenERP helps you managing all your vehicles, the +With this module, Odoo helps you managing all your vehicles, the contracts associated to those vehicle as well as services, fuel log entries, costs and many other features necessary to the management of your fleet of vehicle(s) @@ -56,7 +56,6 @@ Main Features 'fleet_data.xml', 'fleet_board_view.xml', ], - 'images': ['images/costs_analysis.jpeg','images/indicative_costs_analysis.jpeg','images/vehicles.jpeg','images/vehicles_contracts.jpeg','images/vehicles_fuel.jpeg','images/vehicles_odometer.jpeg','images/vehicles_services.jpeg'], 'demo': ['fleet_demo.xml'], diff --git a/addons/fleet/fleet_view.xml b/addons/fleet/fleet_view.xml index ed22f2f922d..e1d66457e88 100644 --- a/addons/fleet/fleet_view.xml +++ b/addons/fleet/fleet_view.xml @@ -285,8 +285,8 @@ </group> </sheet> <div class="oe_chatter"> - <field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/> <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/> </div> </form> </field> diff --git a/addons/gamification/tests/__init__.py b/addons/gamification/tests/__init__.py index 9cafd242b28..49ae47e17c2 100644 --- a/addons/gamification/tests/__init__.py +++ b/addons/gamification/tests/__init__.py @@ -20,7 +20,3 @@ ############################################################################## from . import test_challenge - -checks = [ - test_challenge, -] diff --git a/addons/hr/__openerp__.py b/addons/hr/__openerp__.py index 8239cfa4ebd..ba9cb8798e0 100644 --- a/addons/hr/__openerp__.py +++ b/addons/hr/__openerp__.py @@ -42,12 +42,6 @@ You can manage: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': [ - 'images/hr_department.jpeg', - 'images/hr_employee.jpeg', - 'images/hr_job_position.jpeg', - 'static/src/img/default_image.png', - ], 'depends': ['base_setup','mail', 'resource', 'board'], 'data': [ 'security/hr_security.xml', diff --git a/addons/hr_attendance/__openerp__.py b/addons/hr_attendance/__openerp__.py index 146ca202a83..1574129fc81 100644 --- a/addons/hr_attendance/__openerp__.py +++ b/addons/hr_attendance/__openerp__.py @@ -33,7 +33,6 @@ actions(Sign in/Sign out) performed by them. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': ['images/hr_attendances.jpeg'], 'depends': ['hr', 'report'], 'data': [ 'security/ir_rule.xml', diff --git a/addons/hr_contract/__openerp__.py b/addons/hr_contract/__openerp__.py index 7aaeaa7014f..90c650d101e 100644 --- a/addons/hr_contract/__openerp__.py +++ b/addons/hr_contract/__openerp__.py @@ -37,7 +37,6 @@ You can assign several contracts per employee. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': ['images/hr_contract.jpeg'], 'depends': ['base_action_rule', 'hr'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/hr_evaluation/__openerp__.py b/addons/hr_evaluation/__openerp__.py index b00b37c4d51..7b0a458a8b2 100644 --- a/addons/hr_evaluation/__openerp__.py +++ b/addons/hr_evaluation/__openerp__.py @@ -27,9 +27,6 @@ 'sequence': 31, 'website': 'https://www.odoo.com/page/appraisal', 'summary': 'Periodical Evaluations, Appraisals, Surveys', - 'images': ['images/hr_evaluation_analysis.jpeg', - 'images/hr_evaluation.jpeg', - 'images/hr_interview_requests.jpeg'], 'depends': ['hr', 'calendar', 'survey'], 'description': """ Periodical Employees evaluation and appraisals diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py index 77a1974abfe..05a75c83e85 100644 --- a/addons/hr_expense/__openerp__.py +++ b/addons/hr_expense/__openerp__.py @@ -45,7 +45,6 @@ This module also uses analytic accounting and is compatible with the invoice on """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/expenses', - 'images': ['images/hr_expenses_analysis.jpeg', 'images/hr_expenses.jpeg'], 'depends': ['hr', 'account_accountant', 'report'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/hr_holidays/__openerp__.py b/addons/hr_holidays/__openerp__.py index 07ba46b13b5..4f1b2e2265d 100644 --- a/addons/hr_holidays/__openerp__.py +++ b/addons/hr_holidays/__openerp__.py @@ -44,7 +44,6 @@ You can keep track of leaves in different ways by following reports: A synchronization with an internal agenda (Meetings of the CRM module) is also possible in order to automatically create a meeting when a holiday request is accepted by setting up a type of meeting in Leave Type. """, - 'images': ['images/hr_allocation_requests.jpeg', 'images/hr_leave_requests.jpeg', 'images/leaves_analysis.jpeg'], 'depends': ['hr', 'calendar', 'resource'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index b8ec3793d71..a7f7d72b6c8 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -53,14 +53,14 @@ class hr_holidays_status(osv.osv): # note: add only validated allocation even for the virtual # count; otherwise pending then refused allocation allow # the employee to create more leaves than possible - status_dict['virtual_remaining_leaves'] += holiday.number_of_days - status_dict['max_leaves'] += holiday.number_of_days - status_dict['remaining_leaves'] += holiday.number_of_days + status_dict['virtual_remaining_leaves'] += holiday.number_of_days_temp + status_dict['max_leaves'] += holiday.number_of_days_temp + status_dict['remaining_leaves'] += holiday.number_of_days_temp elif holiday.type == 'remove': # number of days is negative - status_dict['virtual_remaining_leaves'] += holiday.number_of_days + status_dict['virtual_remaining_leaves'] -= holiday.number_of_days_temp if holiday.state == 'validate': - status_dict['leaves_taken'] -= holiday.number_of_days - status_dict['remaining_leaves'] += holiday.number_of_days + status_dict['leaves_taken'] += holiday.number_of_days_temp + status_dict['remaining_leaves'] -= holiday.number_of_days_temp return result def _user_left_days(self, cr, uid, ids, name, args, context=None): diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py index 08642bb5645..2d8d5ee4cdf 100644 --- a/addons/hr_holidays/tests/__init__.py +++ b/addons/hr_holidays/tests/__init__.py @@ -19,8 +19,5 @@ # ############################################################################## -from openerp.addons.hr_holidays.tests import test_holidays_flow +from . import test_holidays_flow -checks = [ - test_holidays_flow, -] diff --git a/addons/hr_payroll/__openerp__.py b/addons/hr_payroll/__openerp__.py index f444cbb5731..f5d0a7c37ae 100644 --- a/addons/hr_payroll/__openerp__.py +++ b/addons/hr_payroll/__openerp__.py @@ -40,12 +40,6 @@ Generic Payroll system. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': [ - 'images/hr_company_contributions.jpeg', - 'images/hr_salary_heads.jpeg', - 'images/hr_salary_structure.jpeg', - 'images/hr_employee_payslip.jpeg' - ], 'depends': [ 'hr', 'hr_contract', diff --git a/addons/hr_payroll_account/__openerp__.py b/addons/hr_payroll_account/__openerp__.py index dedc9f260dd..1bd591c8706 100644 --- a/addons/hr_payroll_account/__openerp__.py +++ b/addons/hr_payroll_account/__openerp__.py @@ -32,7 +32,6 @@ Generic Payroll system Integrated with Accounting. * Company Contribution Management """, 'author':'OpenERP SA', - 'images': ['images/hr_employee_payslip.jpeg'], 'website': 'https://www.odoo.com/page/employees', 'depends': [ 'hr_payroll', diff --git a/addons/hr_recruitment/__openerp__.py b/addons/hr_recruitment/__openerp__.py index 20745f84c01..3ab147ddf2f 100644 --- a/addons/hr_recruitment/__openerp__.py +++ b/addons/hr_recruitment/__openerp__.py @@ -37,8 +37,6 @@ You can define the different phases of interviews and easily rate the applicant """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/recruitment', - 'images': ['images/hr_recruitment_analysis.jpeg', - 'images/hr_recruitment_applicants.jpeg'], 'depends': [ 'decimal_precision', 'hr', diff --git a/addons/hr_timesheet/__openerp__.py b/addons/hr_timesheet/__openerp__.py index 7a0e9a66139..bb554756d09 100644 --- a/addons/hr_timesheet/__openerp__.py +++ b/addons/hr_timesheet/__openerp__.py @@ -40,7 +40,6 @@ up a management by affair. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': ['images/hr_timesheet_lines.jpeg'], 'depends': ['account', 'hr', 'base', 'hr_attendance'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/hr_timesheet_invoice/__openerp__.py b/addons/hr_timesheet_invoice/__openerp__.py index 8fceff84d08..1eb003b1e4f 100644 --- a/addons/hr_timesheet_invoice/__openerp__.py +++ b/addons/hr_timesheet_invoice/__openerp__.py @@ -34,7 +34,6 @@ You can define price lists in analytic account, make some theoretical revenue reports.""", 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': ['images/hr_bill_task_work.jpeg','images/hr_type_of_invoicing.jpeg'], 'depends': ['account', 'hr_timesheet', 'report'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py index 287b011719b..4c3f861a399 100644 --- a/addons/hr_timesheet_sheet/__openerp__.py +++ b/addons/hr_timesheet_sheet/__openerp__.py @@ -45,7 +45,6 @@ The validation can be configured in the company: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/employees', - 'images': ['images/hr_my_current_timesheet.jpeg','images/hr_timesheet_analysis.jpeg','images/hr_timesheet_sheet_analysis.jpeg','images/hr_timesheet_activity.jpeg'], 'depends': ['hr_timesheet', 'hr_timesheet_invoice'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py index a10cf4e84a3..7a8b2d1f727 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py @@ -228,7 +228,7 @@ class hr_timesheet_sheet(osv.osv): def _sheet_date(self, cr, uid, ids, forced_user_id=False, context=None): for sheet in self.browse(cr, uid, ids, context=context): - new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id + new_user_id = forced_user_id or sheet.employee_id.user_id and sheet.employee_id.user_id.id if new_user_id: cr.execute('SELECT id \ FROM hr_timesheet_sheet_sheet \ @@ -591,16 +591,24 @@ class hr_timesheet_sheet_sheet_day(osv.osv): ) union ( select -min(a.id) as id, - a.name::date as name, + (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date as name, s.id as sheet_id, 0.0 as total_timesheet, - SUM(((EXTRACT(hour FROM a.name) * 60) + EXTRACT(minute FROM a.name)) * (CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END)) as total_attendance + SUM(((EXTRACT(hour FROM (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))) * 60) + EXTRACT(minute FROM (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC')))) * (CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END)) as total_attendance from hr_attendance a LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = a.sheet_id + JOIN hr_employee e + ON a.employee_id = e.id + JOIN resource_resource r + ON e.resource_id = r.id + LEFT JOIN res_users u + ON r.user_id = u.id + LEFT JOIN res_partner p + ON u.partner_id = p.id WHERE action in ('sign_in', 'sign_out') - group by a.name::date, s.id + group by (a.name AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id )) AS foo GROUP BY name, sheet_id )) AS bar""") diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 6cf639348bf..25e33c409fa 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -31,8 +31,8 @@ index_template = """ <p> The PosBox is an hardware adapter that allows you to use receipt printers and barcode scanners with Odoo's Point of - Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.openerp.com/start'>online free trial</a>, - or <a href='https://www.openerp.com/start?download'>download and install</a> it yourself. + Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.odoo.com/start'>online free trial</a>, + or <a href='https://www.odoo.com/start?download'>download and install</a> it yourself. </p> <p> For more information on how to setup the Point of Sale with diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index 56e94d88a6d..64f6cf0765c 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -98,7 +98,7 @@ class Proxy(http.Controller): <p> Copy and paste your printer's device description in the form below. You can find your printer's description in the device list above. If you find that your printer works - well, please send your printer's description to <a href='mailto:support@openerp.com'> + well, please send your printer's description to <a href='mailto:support@odoo.com'> support@openerp.com</a> so that we can add it to the default list of supported devices. </p> <form action='/hw_proxy/escpos/add_supported_device' method='GET'> diff --git a/addons/knowledge/__openerp__.py b/addons/knowledge/__openerp__.py index df403fdd15b..10c251ee198 100644 --- a/addons/knowledge/__openerp__.py +++ b/addons/knowledge/__openerp__.py @@ -43,5 +43,4 @@ document and Wiki based Hidden. 'demo': ['knowledge_demo.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/1_config_knowledge.jpeg'], } diff --git a/addons/l10n_ar/__openerp__.py b/addons/l10n_ar/__openerp__.py index 2ba89d58431..9b692a89a08 100644 --- a/addons/l10n_ar/__openerp__.py +++ b/addons/l10n_ar/__openerp__.py @@ -41,5 +41,4 @@ Plan contable argentino e impuestos de acuerdo a disposiciones vigentes 'demo': [], 'active': False, 'installable': True, - 'images': ['images/config_chart_l10n_ar.jpeg','images/l10n_ar_chart.jpeg'], } diff --git a/addons/l10n_be/__openerp__.py b/addons/l10n_be/__openerp__.py index b38001e5fe3..01750190fe6 100644 --- a/addons/l10n_be/__openerp__.py +++ b/addons/l10n_be/__openerp__.py @@ -83,5 +83,4 @@ Wizards provided by this module: 'demo': [], 'installable': True, 'website': 'https://www.odoo.com/page/accounting', - 'images': ['images/1_config_chart_l10n_be.jpeg','images/2_l10n_be_chart.jpeg'], } diff --git a/addons/l10n_be_coda/__openerp__.py b/addons/l10n_be_coda/__openerp__.py index 192f2dc45f9..334cadae180 100644 --- a/addons/l10n_be_coda/__openerp__.py +++ b/addons/l10n_be_coda/__openerp__.py @@ -71,7 +71,6 @@ description provided by the CODA configuration tables is based upon the CODA V2.2 specifications. If required, you can manually adjust the descriptions via the CODA configuration menu. ''', - 'images': ['images/coda_logs.jpeg', 'images/import_coda_logs.jpeg'], 'depends': ['account_voucher', 'base_iban', 'l10n_be_invoice_bba', 'account_bank_statement_import'], 'demo': ['l10n_be_coda_demo.xml'], 'data': [ diff --git a/addons/l10n_be_coda/wizard/account_coda_import.py b/addons/l10n_be_coda/wizard/account_coda_import.py index bca1f989853..476f4bf8a0e 100644 --- a/addons/l10n_be_coda/wizard/account_coda_import.py +++ b/addons/l10n_be_coda/wizard/account_coda_import.py @@ -57,6 +57,7 @@ class account_bank_statement_import(osv.TransientModel): statement['version'] = line[127] if statement['version'] not in ['1', '2']: raise UserError(_('Error') + ' R001: ' + _('CODA V%s statements are not supported, please contact your bank') % statement['version']) + statement['globalisation_stack'] = [] statement['lines'] = [] statement['date'] = time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT, time.strptime(rmspaces(line[5:11]), '%d%m%y')) statement['separateApplication'] = rmspaces(line[83:88]) @@ -116,7 +117,11 @@ class account_bank_statement_import(osv.TransientModel): statementLine['type'] = 'normal' statementLine['globalisation'] = int(line[124]) if statementLine['globalisation'] > 0: - statementLine['type'] = 'globalisation' + if statementLine['globalisation'] in statement['globalisation_stack']: + statement['globalisation_stack'].remove(statementLine['globalisation']) + else: + statementLine['type'] = 'globalisation' + statement['globalisation_stack'].append(statementLine['globalisation']) globalisation_comm[statementLine['ref_move']] = statementLine['communication'] if not statementLine.get('communication'): statementLine['communication'] = globalisation_comm.get(statementLine['ref_move'], '') diff --git a/addons/l10n_bo/__openerp__.py b/addons/l10n_bo/__openerp__.py index 2928017bcdb..71487288ba2 100644 --- a/addons/l10n_bo/__openerp__.py +++ b/addons/l10n_bo/__openerp__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # -##############################################################################
+############################################################################## { "name": "Bolivia Localization Chart Account", "version": "1.0", @@ -46,6 +46,5 @@ Plan contable boliviano e impuestos de acuerdo a disposiciones vigentes "active": False, "installable": True, "certificate" : "", - 'images': ['images/config_chart_l10n_bo.jpeg','images/l10n_bo_chart.jpeg'], } diff --git a/addons/l10n_ca/__openerp__.py b/addons/l10n_ca/__openerp__.py index fa5c3a79667..3f8667056d5 100644 --- a/addons/l10n_ca/__openerp__.py +++ b/addons/l10n_ca/__openerp__.py @@ -75,5 +75,4 @@ position. ], 'demo': [], 'installable': True, - 'images': ['images/config_chart_l10n_ca.jpeg','images/l10n_ca_chart.jpeg'], } diff --git a/addons/l10n_ch/__openerp__.py b/addons/l10n_ch/__openerp__.py index 38500842145..31ed150c9aa 100644 --- a/addons/l10n_ch/__openerp__.py +++ b/addons/l10n_ch/__openerp__.py @@ -70,5 +70,4 @@ https://github.com/OCA/l10n-switzerland 'test': [], 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_ch.jpeg', 'images/l10n_ch_chart.jpeg'] } diff --git a/addons/l10n_cl/__openerp__.py b/addons/l10n_cl/__openerp__.py index b4bf884182d..1e16e486508 100644 --- a/addons/l10n_cl/__openerp__.py +++ b/addons/l10n_cl/__openerp__.py @@ -40,5 +40,4 @@ Plan contable chileno e impuestos de acuerdo a disposiciones vigentes 'demo': [], 'active': False, 'installable': True, - 'images': ['images/config_chart_l10n_cl.jpeg','images/l10n_cl_chart.jpeg'], } diff --git a/addons/l10n_cn/__openerp__.py b/addons/l10n_cn/__openerp__.py index ca2b8f9fcad..45507b64c75 100644 --- a/addons/l10n_cn/__openerp__.py +++ b/addons/l10n_cn/__openerp__.py @@ -49,5 +49,4 @@ 'license': 'GPL-3', 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_cn.jpeg','images/l10n_cn_chart.jpeg'], } diff --git a/addons/l10n_co/__openerp__.py b/addons/l10n_co/__openerp__.py index fd47a7329ce..8a1f89a2dc0 100644 --- a/addons/l10n_co/__openerp__.py +++ b/addons/l10n_co/__openerp__.py @@ -45,5 +45,4 @@ ], 'demo': [], 'installable': True, - 'images': [], } diff --git a/addons/l10n_cr/__openerp__.py b/addons/l10n_cr/__openerp__.py index 63fa88118f5..e2f75c6bc0f 100644 --- a/addons/l10n_cr/__openerp__.py +++ b/addons/l10n_cr/__openerp__.py @@ -68,5 +68,4 @@ please go to http://translations.launchpad.net/openerp-costa-rica. 'license': 'Other OSI approved licence', 'installable': True, 'auto_install': False, - 'images': ['images/config_chart_l10n_cr.jpeg','images/l10n_cr_chart.jpeg'], } diff --git a/addons/l10n_de/__openerp__.py b/addons/l10n_de/__openerp__.py index 2ee625bd866..503b1f141ce 100644 --- a/addons/l10n_de/__openerp__.py +++ b/addons/l10n_de/__openerp__.py @@ -148,5 +148,4 @@ German accounting chart and localization. 'l10n_de_wizard.xml', ], 'installable': True, - 'images': ['images/config_chart_l10n_de.jpeg','images/l10n_de_chart.jpeg'], } diff --git a/addons/l10n_ec/__openerp__.py b/addons/l10n_ec/__openerp__.py index ee97c6ca771..376f373e66a 100644 --- a/addons/l10n_ec/__openerp__.py +++ b/addons/l10n_ec/__openerp__.py @@ -42,5 +42,4 @@ Accounting chart and localization for Ecuador. ], 'demo': [], 'installable': True, - 'images': ['images/config_chart_l10n_ec.jpeg','images/l10n_ec_chart.jpeg'], } diff --git a/addons/l10n_fr/__openerp__.py b/addons/l10n_fr/__openerp__.py index f094fbc10a8..5d19af3f237 100644 --- a/addons/l10n_fr/__openerp__.py +++ b/addons/l10n_fr/__openerp__.py @@ -73,5 +73,4 @@ configuration of their taxes and fiscal positions manually. 'demo': [], 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_fr.jpeg','images/l10n_fr_chart.jpeg'], } diff --git a/addons/l10n_gr/__openerp__.py b/addons/l10n_gr/__openerp__.py index 8ea37a8688f..9224dddc835 100644 --- a/addons/l10n_gr/__openerp__.py +++ b/addons/l10n_gr/__openerp__.py @@ -41,5 +41,4 @@ Greek accounting chart and localization. 'l10n_gr_wizard.xml' ], 'installable': True, - 'images': ['images/config_chart_l10n_gr.jpeg','images/l10n_gr_chart.jpeg'], } diff --git a/addons/l10n_gt/__openerp__.py b/addons/l10n_gt/__openerp__.py index 7c13170b576..54356e70cd8 100644 --- a/addons/l10n_gt/__openerp__.py +++ b/addons/l10n_gt/__openerp__.py @@ -54,5 +54,4 @@ taxes and the Quetzal currency.""", ], 'demo': [], 'installable': True, - 'images': ['images/config_chart_l10n_gt.jpeg','images/l10n_gt_chart.jpeg'], } diff --git a/addons/l10n_in/__openerp__.py b/addons/l10n_in/__openerp__.py index a45a6d9cf02..a105f97b931 100644 --- a/addons/l10n_in/__openerp__.py +++ b/addons/l10n_in/__openerp__.py @@ -51,5 +51,4 @@ Sheet, now only Vertical format has been permitted Which is Supported By OpenERP ], 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_in.jpeg','images/l10n_in_chart.jpeg'], } diff --git a/addons/l10n_it/__openerp__.py b/addons/l10n_it/__openerp__.py index 686fb5ff0d4..807f78cfd5f 100644 --- a/addons/l10n_it/__openerp__.py +++ b/addons/l10n_it/__openerp__.py @@ -52,5 +52,4 @@ Italian accounting chart and localization. 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/config_chart_l10n_it.jpeg','images/l10n_it_chart.jpeg'], } diff --git a/addons/l10n_lu/__openerp__.py b/addons/l10n_lu/__openerp__.py index a147cfe02e1..8f928f3a589 100644 --- a/addons/l10n_lu/__openerp__.py +++ b/addons/l10n_lu/__openerp__.py @@ -56,5 +56,4 @@ This is the base module to manage the accounting chart for Luxembourg. 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/config_chart_l10n_lu.jpeg','images/l10n_lu_chart.jpeg'], } diff --git a/addons/l10n_ma/__openerp__.py b/addons/l10n_ma/__openerp__.py index 422300f5fd4..d3824a249a3 100644 --- a/addons/l10n_ma/__openerp__.py +++ b/addons/l10n_ma/__openerp__.py @@ -46,5 +46,4 @@ Seddik au cours du troisième trimestre 2010.""", 'demo' : [], 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_ma.jpeg','images/l10n_ma_chart.jpeg'], } diff --git a/addons/l10n_nl/__openerp__.py b/addons/l10n_nl/__openerp__.py index 68012a2a8a1..83b3ee1ed96 100644 --- a/addons/l10n_nl/__openerp__.py +++ b/addons/l10n_nl/__openerp__.py @@ -130,5 +130,4 @@ De extra cijfers worden dan achter het rekeningnummer aangevult met 'nullen'. ], 'demo' : [], 'installable': True, - 'images': ['images/config_chart_l10n_nl.jpeg','images/l10n_nl_chart.jpeg'], } diff --git a/addons/l10n_pa/__openerp__.py b/addons/l10n_pa/__openerp__.py index be04eec3cd2..18f0f545792 100644 --- a/addons/l10n_pa/__openerp__.py +++ b/addons/l10n_pa/__openerp__.py @@ -47,5 +47,4 @@ Con la Colaboración de "active": False, "installable": True, "certificate" : "", - 'images': ['images/config_chart_l10n_cl.jpeg','images/l10n_cl_chart.jpeg'], } diff --git a/addons/l10n_pe/__openerp__.py b/addons/l10n_pe/__openerp__.py index 5b3b76fe684..ebb96778eb7 100644 --- a/addons/l10n_pe/__openerp__.py +++ b/addons/l10n_pe/__openerp__.py @@ -42,5 +42,4 @@ SUNAT 2011 (PCGE 2010). 'demo': [], 'active': False, 'installable': True, - 'images': ['images/config_chart_l10n_pe.jpeg','images/l10n_pe_chart.jpeg'], } diff --git a/addons/l10n_th/__openerp__.py b/addons/l10n_th/__openerp__.py index cf25e7cbccc..1b4e39fe7e8 100644 --- a/addons/l10n_th/__openerp__.py +++ b/addons/l10n_th/__openerp__.py @@ -34,5 +34,4 @@ Thai accounting chart and localization. 'depends': ['account_chart'], 'data': [ 'account_data.xml' ], 'installable': True, - 'images': ['images/config_chart_l10n_th.jpeg','images/l10n_th_chart.jpeg'], } diff --git a/addons/l10n_tr/__openerp__.py b/addons/l10n_tr/__openerp__.py index 302076a7344..b4b43c7d403 100644 --- a/addons/l10n_tr/__openerp__.py +++ b/addons/l10n_tr/__openerp__.py @@ -48,5 +48,4 @@ Bu modül kurulduktan sonra, Muhasebe yapılandırma sihirbazı çalışır ], 'demo': [], 'installable': True, - 'images': ['images/chart_l10n_tr_1.jpg','images/chart_l10n_tr_2.jpg','images/chart_l10n_tr_3.jpg'], } diff --git a/addons/l10n_uk/__openerp__.py b/addons/l10n_uk/__openerp__.py index c50deea2e96..1d08b62846f 100644 --- a/addons/l10n_uk/__openerp__.py +++ b/addons/l10n_uk/__openerp__.py @@ -44,5 +44,4 @@ This is the latest UK OpenERP localisation necessary to run OpenERP accounting f ], 'demo' : ['demo/demo.xml'], 'installable': 'True', - 'images': ['images/config_chart_l10n_uk.jpeg','images/l10n_uk_chart.jpeg'], } diff --git a/addons/l10n_uy/__openerp__.py b/addons/l10n_uy/__openerp__.py index 5e967529225..ff4ac166877 100644 --- a/addons/l10n_uy/__openerp__.py +++ b/addons/l10n_uy/__openerp__.py @@ -46,5 +46,4 @@ Provide Templates for Chart of Accounts, Taxes for Uruguay. 'demo': [], 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_uy.jpeg','images/l10n_uy_chart.jpeg'], } diff --git a/addons/l10n_ve/__openerp__.py b/addons/l10n_ve/__openerp__.py index 2b865fdf1f3..06da8b8bc33 100644 --- a/addons/l10n_ve/__openerp__.py +++ b/addons/l10n_ve/__openerp__.py @@ -71,6 +71,4 @@ but you will need set manually account defaults for taxes. ], 'auto_install': False, 'installable': True, - 'images': ['images/config_chart_l10n_ve.jpeg', - 'images/l10n_ve_chart.jpeg'], } diff --git a/addons/lunch/__openerp__.py b/addons/lunch/__openerp__.py index 951f4c3a4bf..aec7b17ff71 100644 --- a/addons/lunch/__openerp__.py +++ b/addons/lunch/__openerp__.py @@ -52,11 +52,9 @@ If you want to save your employees' time and avoid them to always have coins in 'views/report_lunchorder.xml', 'views/lunch.xml', ], - 'images': ['images/new_order.jpeg','images/lunch_account.jpeg','images/order_by_supplier_analysis.jpeg','images/alert.jpeg'], 'demo': ['lunch_demo.xml',], 'installable': True, 'website': 'https://www.odoo.com/page/employees', 'application' : True, 'certificate' : '001292377792581874189', - 'images': [], } diff --git a/addons/lunch/tests/__init__.py b/addons/lunch/tests/__init__.py index 34446b6d88a..fd218c34e65 100644 --- a/addons/lunch/tests/__init__.py +++ b/addons/lunch/tests/__init__.py @@ -20,6 +20,3 @@ ############################################################################## from . import test_lunch -checks = [ - test_lunch, -] diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 642aeb1bca8..a7a96617119 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -75,14 +75,6 @@ Main Features ], 'installable': True, 'application': True, - 'images': [ - 'images/inbox.jpeg', - 'images/messages_form.jpeg', - 'images/messages_list.jpeg', - 'images/email.jpeg', - 'images/join_a_group.jpeg', - 'images/share_a_message.jpeg', - ], 'qweb': [ 'static/src/xml/mail.xml', 'static/src/xml/mail_followers.xml', diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 254cd193285..4e3e77f1dd8 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -1443,18 +1443,34 @@ class mail_thread(osv.AbstractModel): if follower.email == email_address: partner_id = follower.id # second try: check in partners that are also users + # Escape special SQL characters in email_address to avoid invalid matches + email_address = (email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')) + email_brackets = "<%s>" % email_address if not partner_id: - ids = partner_obj.search(cr, SUPERUSER_ID, [ - ('email', 'ilike', email_address), - ('user_ids', '!=', False) - ], limit=1, context=context) + # exact, case-insensitive match + ids = partner_obj.search(cr, SUPERUSER_ID, + [('email', '=ilike', email_address), + ('user_ids', '!=', False)], + limit=1, context=context) + if not ids: + # if no match with addr-spec, attempt substring match within name-addr pair + ids = partner_obj.search(cr, SUPERUSER_ID, + [('email', 'ilike', email_brackets), + ('user_ids', '!=', False)], + limit=1, context=context) if ids: partner_id = ids[0] # third try: check in partners if not partner_id: - ids = partner_obj.search(cr, SUPERUSER_ID, [ - ('email', 'ilike', email_address) - ], limit=1, context=context) + # exact, case-insensitive match + ids = partner_obj.search(cr, SUPERUSER_ID, + [('email', '=ilike', email_address)], + limit=1, context=context) + if not ids: + # if no match with addr-spec, attempt substring match within name-addr pair + ids = partner_obj.search(cr, SUPERUSER_ID, + [('email', 'ilike', email_brackets)], + limit=1, context=context) if ids: partner_id = ids[0] partner_ids.append(partner_id) @@ -1474,13 +1490,15 @@ class mail_thread(osv.AbstractModel): partner_id = partner_ids[idx] partner_info = {'full_name': email_address, 'partner_id': partner_id} result.append(partner_info) - # link mail with this from mail to the new partner id if link_mail and partner_info['partner_id']: + # Escape special SQL characters in email_address to avoid invalid matches + email_address = (email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')) + email_brackets = "<%s>" % email_address message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [ '|', - ('email_from', '=', email_address), - ('email_from', 'ilike', '<%s>' % email_address), + ('email_from', '=ilike', email_address), + ('email_from', 'ilike', email_brackets), ('author_id', '=', False) ], context=context) if message_ids: diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 59c1eaecd89..d54ad6176b1 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -369,6 +369,16 @@ openerp.mail = function (session) { this.is_log = false; this.recipients = []; this.recipient_ids = []; + session.web.bus.on('clear_uncommitted_changes', this, function(e) { + if (this.show_composer && !e.isDefaultPrevented()){ + if (!confirm(_t("You are currently composing a message, your message will be discarded.\n\nAre you sure you want to leave this page ?"))) { + e.preventDefault(); + } + else{ + this.on_cancel(); + } + } + }); }, start: function () { diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py index e5592468cb4..23162ee1c2f 100644 --- a/addons/mail/tests/__init__.py +++ b/addons/mail/tests/__init__.py @@ -20,14 +20,3 @@ ############################################################################## from . import test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite, test_mail, test_ir_actions - -checks = [ - test_mail_group, - test_mail_message, - test_mail_features, - test_mail_gateway, - test_message_read, - test_invite, - test_mail, - test_ir_actions, -] diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index 5de41cc5d7a..cc732b007a2 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -68,7 +68,7 @@ MAIL_TEMPLATE_PLAINTEXT = """Return-Path: <whatever-2a840@postmaster.twitter.com To: {to} Received: by mail1.openerp.com (Postfix, from userid 10002) id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST) -From: Sylvie Lelitre <sylvie.lelitre@agrolait.com> +From: Sylvie Lelitre <test.sylvie.lelitre@agrolait.com> Subject: {subject} MIME-Version: 1.0 Content-Type: text/plain @@ -525,6 +525,16 @@ class TestMailgateway(TestMail): self.assertEqual(frog_group.message_ids[0].author_id.id, extra_partner_id, 'message_process: email_from -> author_id wrong') + # Do: post a new message with a non-existant email that is a substring of a partner email + format_and_process(MAIL_TEMPLATE, email_from='Not really Lombrik Lubrik <oul@email.com>', + subject='Re: news (2)', + msg_id='<zzzbbbaaaa@agrolait.com>', + extra='In-Reply-To: <1198923581.41972151344608186760.JavaMail@agrolait.com>\n') + frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')]) + frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) + # Test: author must not be set, otherwise the system is confusing different users + self.assertFalse(frog_group.message_ids[0].author_id, 'message_process: email_from -> mismatching author_id') + # Do: post a new message, with a known partner -> duplicate emails -> user frog_group.message_unsubscribe([extra_partner_id]) self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'}) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 49eaa599888..485869dd6fe 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -286,7 +286,7 @@ class mail_compose_message(osv.TransientModel): batch_mail_mail_ids.append(mail_mail_id) else: subtype = 'mail.mt_comment' - if context.get('mail_compose_log') or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False + if wizard.is_log or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False subtype = False if wizard.composition_mode == 'mass_post': context = dict(context, diff --git a/addons/marketing/__openerp__.py b/addons/marketing/__openerp__.py index fda4d8d950b..556ca74d2ad 100644 --- a/addons/marketing/__openerp__.py +++ b/addons/marketing/__openerp__.py @@ -21,5 +21,4 @@ Contains the installer for marketing-related modules. 'demo': ['marketing_demo.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/config_marketing.jpeg'], } diff --git a/addons/marketing_campaign/__openerp__.py b/addons/marketing_campaign/__openerp__.py index f2512a74ff2..c0eac4eff41 100644 --- a/addons/marketing_campaign/__openerp__.py +++ b/addons/marketing_campaign/__openerp__.py @@ -67,5 +67,4 @@ input segments, workflow. 'test': ['test/marketing_campaign.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/campaign.png', 'images/campaigns.jpeg','images/email_account.jpeg','images/email_templates.jpeg','images/segments.jpeg'], } diff --git a/addons/marketing_campaign_crm_demo/__openerp__.py b/addons/marketing_campaign_crm_demo/__openerp__.py index 21953d88424..703b7b58536 100644 --- a/addons/marketing_campaign_crm_demo/__openerp__.py +++ b/addons/marketing_campaign_crm_demo/__openerp__.py @@ -39,5 +39,4 @@ Creates demo data like leads, campaigns and segments for the module marketing_ca 'demo': ['marketing_campaign_demo.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/campaigns.jpeg','images/email_templates.jpeg'], } diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py index f8610905393..0bc1d8747a8 100644 --- a/addons/mass_mailing/tests/__init__.py +++ b/addons/mass_mailing/tests/__init__.py @@ -19,8 +19,4 @@ # ############################################################################## -from openerp.addons.mass_mailing.tests import test_mail - -checks = [ - test_mail, -] +from . import test_mail diff --git a/addons/membership/__openerp__.py b/addons/membership/__openerp__.py index ed4ecee78f5..f259bac2aac 100644 --- a/addons/membership/__openerp__.py +++ b/addons/membership/__openerp__.py @@ -54,5 +54,4 @@ invoice and send propositions for membership renewal. 'test': ['test/test_membership.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/members.jpeg','images/membership_list.jpeg', 'images/membership_products.jpeg'], } diff --git a/addons/mrp/__openerp__.py b/addons/mrp/__openerp__.py index 7a1b7c9e706..32e6f22e25b 100644 --- a/addons/mrp/__openerp__.py +++ b/addons/mrp/__openerp__.py @@ -28,7 +28,6 @@ 'category': 'Manufacturing', 'sequence': 18, 'summary': 'Manufacturing Orders, Bill of Materials, Routing', - 'images': ['images/bill_of_materials.jpeg', 'images/manufacturing_order.jpeg', 'images/planning_manufacturing_order.jpeg', 'images/manufacturing_analysis.jpeg', 'images/routings.jpeg','images/work_centers.jpeg'], 'depends': ['product', 'procurement', 'stock_account', 'resource', 'report'], 'description': """ Manage the Manufacturing process in OpenERP diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index 74fdbaea709..dd089e728f7 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -22,7 +22,7 @@ import time import openerp.addons.decimal_precision as dp from openerp.osv import fields, osv -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT from openerp.tools.translate import _ from openerp import tools, SUPERUSER_ID from openerp.addons.product import _common @@ -217,8 +217,8 @@ class mrp_bom(osv.osv): else: # neither product nor template, makes no sense to search return False - domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), - '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] + domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATE_FORMAT)), + '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT))] # order to prioritize bom with product_id over the one without ids = self.search(cr, uid, domain, order='product_id', context=context) # Search a BoM which has all properties specified, or if you can not find one, you could @@ -277,8 +277,8 @@ class mrp_bom(osv.osv): }) for bom_line_id in bom.bom_line_ids: - if bom_line_id.date_start and bom_line_id.date_start > time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or \ - bom_line_id.date_stop and bom_line_id.date_stop < time.strftime(DEFAULT_SERVER_DATETIME_FORMAT): + if bom_line_id.date_start and bom_line_id.date_start > time.strftime(DEFAULT_SERVER_DATE_FORMAT) or \ + bom_line_id.date_stop and bom_line_id.date_stop < time.strftime(DEFAULT_SERVER_DATE_FORMAT): continue # all bom_line_id variant values must be in the product if bom_line_id.attribute_value_ids: @@ -678,6 +678,28 @@ class mrp_production(osv.osv): return {'value': result} + def _prepare_lines(self, cr, uid, production, properties=None, context=None): + # search BoM structure and route + bom_obj = self.pool.get('mrp.bom') + uom_obj = self.pool.get('product.uom') + bom_point = production.bom_id + bom_id = production.bom_id.id + if not bom_point: + bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context) + if bom_id: + bom_point = bom_obj.browse(cr, uid, bom_id) + routing_id = bom_point.routing_id.id or False + self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id}) + + if not bom_id: + raise UserError(_("Cannot find a bill of material for this product.")) + + # get components and workcenter_lines from BoM structure + factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id) + # product_lines, workcenter_lines + return bom_obj._bom_explode(cr, uid, bom_point, production.product_id, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id, context=context) + + def _action_compute_lines(self, cr, uid, ids, properties=None, context=None): """ Compute product_lines and workcenter_lines from BoM structure @return: product_lines @@ -685,8 +707,6 @@ class mrp_production(osv.osv): if properties is None: properties = [] results = [] - bom_obj = self.pool.get('mrp.bom') - uom_obj = self.pool.get('product.uom') prod_line_obj = self.pool.get('mrp.production.product.line') workcenter_line_obj = self.pool.get('mrp.production.workcenter.line') for production in self.browse(cr, uid, ids, context=context): @@ -694,23 +714,10 @@ class mrp_production(osv.osv): prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context) #unlink workcenter_lines workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context) - # search BoM structure and route - bom_point = production.bom_id - bom_id = production.bom_id.id - if not bom_point: - bom_id = bom_obj._bom_find(cr, uid, product_id=production.product_id.id, properties=properties, context=context) - if bom_id: - bom_point = bom_obj.browse(cr, uid, bom_id) - routing_id = bom_point.routing_id.id or False - self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id}) - - if not bom_id: - raise UserError(_("Cannot find a bill of material for this product.")) - # get components and workcenter_lines from BoM structure - factor = uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, bom_point.product_uom.id) - # product_lines, workcenter_lines - results, results2 = bom_obj._bom_explode(cr, uid, bom_point, production.product_id, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id, context=context) + res = self._prepare_lines(cr, uid, production, properties=properties, context=context) + results = res[0] # product_lines + results2 = res[1] # workcenter_lines # reset product_lines in production order for line in results: diff --git a/addons/mrp_byproduct/__openerp__.py b/addons/mrp_byproduct/__openerp__.py index 4cb598265c1..8bf6ab02c4c 100644 --- a/addons/mrp_byproduct/__openerp__.py +++ b/addons/mrp_byproduct/__openerp__.py @@ -40,7 +40,6 @@ With this module: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/manufacturing', - 'images': ['images/bom_byproduct.jpeg'], 'depends': ['base', 'mrp'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/mrp_operations/__openerp__.py b/addons/mrp_operations/__openerp__.py index 96cfc97c20e..1154df9bf40 100644 --- a/addons/mrp_operations/__openerp__.py +++ b/addons/mrp_operations/__openerp__.py @@ -54,7 +54,6 @@ So, that we can compare the theoretic delay and real delay. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/manufacturing', - 'images': ['images/work_order_analysis.jpeg','images/work_order_planning.jpeg','images/work_order.jpeg'], 'depends': ['mrp'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/mrp_repair/__openerp__.py b/addons/mrp_repair/__openerp__.py index e64e76fbd9a..f7506d7bce3 100644 --- a/addons/mrp_repair/__openerp__.py +++ b/addons/mrp_repair/__openerp__.py @@ -38,7 +38,6 @@ The following topics should be covered by this module: * Notes for the technician and for the final customer """, 'author': 'OpenERP SA', - 'images': ['images/repair_order.jpeg'], 'depends': ['mrp', 'sale', 'account'], 'website': 'https://www.odoo.com/page/manufacturing', 'data': [ diff --git a/addons/multi_company/__openerp__.py b/addons/multi_company/__openerp__.py index b6ab2dea550..803b311dad9 100644 --- a/addons/multi_company/__openerp__.py +++ b/addons/multi_company/__openerp__.py @@ -41,5 +41,4 @@ This module is the base module for other multi-company modules. 'demo': ['multi_company_demo.xml'], 'installable': True, 'auto_install': False, - 'images': ['images/companies.jpeg','images/default_company_per_object_form.jpeg', 'images/default_company_per_object_list.jpeg'], } diff --git a/addons/note/__openerp__.py b/addons/note/__openerp__.py index a890a91d6d9..63e293ed751 100644 --- a/addons/note/__openerp__.py +++ b/addons/note/__openerp__.py @@ -55,11 +55,6 @@ Notes can be found in the 'Home' menu. ], 'test': [ ], - 'images': [ - 'images/note_kanban.jpeg', - 'images/note.jpeg', - 'images/categories_tree.jpeg' - ], 'installable': True, 'application': True, 'auto_install': False, diff --git a/addons/note/tests/__init__.py b/addons/note/tests/__init__.py index cb56655f546..48ddba22f76 100644 --- a/addons/note/tests/__init__.py +++ b/addons/note/tests/__init__.py @@ -20,6 +20,3 @@ ############################################################################## from . import test_note -checks = [ - test_note, -] diff --git a/addons/pad/__openerp__.py b/addons/pad/__openerp__.py index f9ba238e126..b4bbc722f8a 100644 --- a/addons/pad/__openerp__.py +++ b/addons/pad/__openerp__.py @@ -22,5 +22,4 @@ pads (by default, http://ietherpad.com/). 'auto_install': False, 'web': True, 'qweb' : ['static/src/xml/*.xml'], - 'images': ['static/src/img/pad_link_companies.jpeg'], } diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 002fa1344f3..7a5f541a8f8 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -62,7 +62,8 @@ class PaymentAcquirer(osv.Model): 'name': fields.char('Name', required=True), 'provider': fields.selection(_provider_selection, string='Provider', required=True), 'company_id': fields.many2one('res.company', 'Company', required=True), - 'pre_msg': fields.html('Message', help='Message displayed to explain and help the payment process.'), + 'pre_msg': fields.html('Message', translate=True, + help='Message displayed to explain and help the payment process.'), 'post_msg': fields.html('Thanks Message', help='Message displayed after having done the payment process.'), 'validation': fields.selection( [('manual', 'Manual'), ('automatic', 'Automatic')], diff --git a/addons/payment_adyen/tests/__init__.py b/addons/payment_adyen/tests/__init__.py index 8c8e8249184..40a96afc6ff 100644 --- a/addons/payment_adyen/tests/__init__.py +++ b/addons/payment_adyen/tests/__init__.py @@ -1,7 +1 @@ # -*- coding: utf-8 -*- - -# from . import test_adyen - -checks = [ - # test_adyen, -] diff --git a/addons/payment_buckaroo/tests/__init__.py b/addons/payment_buckaroo/tests/__init__.py index d245ab8339d..22dc419fcfe 100644 --- a/addons/payment_buckaroo/tests/__init__.py +++ b/addons/payment_buckaroo/tests/__init__.py @@ -1,7 +1,3 @@ # -*- coding: utf-8 -*- -from openerp.addons.payment_buckaroo.tests import test_buckaroo - -checks = [ - test_buckaroo, -] +from . import test_buckaroo diff --git a/addons/payment_ogone/tests/__init__.py b/addons/payment_ogone/tests/__init__.py index 5b920121911..40a96afc6ff 100644 --- a/addons/payment_ogone/tests/__init__.py +++ b/addons/payment_ogone/tests/__init__.py @@ -1,7 +1 @@ # -*- coding: utf-8 -*- - -# from . import test_ogone - -checks = [ - # test_ogone, -] diff --git a/addons/payment_paypal/tests/__init__.py b/addons/payment_paypal/tests/__init__.py index 4c38b20e76a..40a96afc6ff 100644 --- a/addons/payment_paypal/tests/__init__.py +++ b/addons/payment_paypal/tests/__init__.py @@ -1,7 +1 @@ # -*- coding: utf-8 -*- - -# from . import test_paypal - -checks = [ - # test_paypal, -] diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index 8e3d9859cc1..68680102328 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -47,7 +47,6 @@ Main Features * Refund previous sales """, 'author': 'OpenERP SA', - 'images': ['images/pos_touch_screen.jpeg', 'images/pos_session.jpeg', 'images/pos_analysis.jpeg','images/sale_order_pos.jpeg','images/product_pos.jpeg'], 'depends': ['sale_stock', 'barcodes'], 'data': [ 'data/report_paperformat.xml', diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 0f26ba5317e..6657341c230 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import logging +import werkzeug.utils from openerp import http from openerp.http import request @@ -19,6 +20,8 @@ class PosController(http.Controller): PosSession = request.registry['pos.session'] pos_session_ids = PosSession.search(cr, uid, [('state','=','opened'),('user_id','=',session.uid)], context=context) + if not pos_session_ids: + return werkzeug.utils.redirect('/web#action=point_of_sale.action_pos_session_opening') PosSession.login(cr, uid, pos_session_ids, context=context) return request.render('point_of_sale.index') diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index a4a0807411e..d644850b798 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -414,7 +414,7 @@ class pos_session(osv.osv): if not pos_config.journal_id: jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] if jid: - jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context) + jobj.write(cr, openerp.SUPERUSER_ID, [pos_config.id], {'journal_id': jid}, context=context) else: raise UserError(_("Unable to open the session. You have to assign a sale journal to your point of sale.")) @@ -427,8 +427,8 @@ class pos_session(osv.osv): if not cashids: cashids = journal_proxy.search(cr, uid, [('journal_user','=',True)], context=context) - journal_proxy.write(cr, uid, cashids, {'journal_user': True}) - jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) + journal_proxy.write(cr, openerp.SUPERUSER_ID, cashids, {'journal_user': True}) + jobj.write(cr, openerp.SUPERUSER_ID, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) pos_config = jobj.browse(cr, uid, config_id, context=context) @@ -589,6 +589,32 @@ class pos_order(osv.osv): 'journal': ui_paymentline['journal_id'], } + def _process_order(self, cr, uid, order, context=None): + order_id = self.create(cr, uid, self._order_fields(cr, uid, order, context=context),context) + + for payments in order['statement_ids']: + self.add_payment(cr, uid, order_id, self._payment_fields(cr, uid, payments[2], context=context), context=context) + + session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) + if session.sequence_number <= order['sequence_number']: + session.write({'sequence_number': order['sequence_number'] + 1}) + session.refresh() + + if order['amount_return']: + cash_journal = session.cash_journal_id + if not cash_journal: + cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids) + if not len(cash_journal_ids): + raise UserError(_("No cash statement found for this session. Unable to record returned cash.")) + cash_journal = cash_journal_ids[0].journal_id + self.add_payment(cr, uid, order_id, { + 'amount': -order['amount_return'], + 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'), + 'payment_name': _('return'), + 'journal': cash_journal.id, + }, context=context) + return order_id + def create_from_ui(self, cr, uid, orders, context=None): # Keep only new orders submitted_references = [o['data']['name'] for o in orders] @@ -602,29 +628,7 @@ class pos_order(osv.osv): for tmp_order in orders_to_save: to_invoice = tmp_order['to_invoice'] order = tmp_order['data'] - order_id = self.create(cr, uid, self._order_fields(cr, uid, order, context=context),context) - - for payments in order['statement_ids']: - self.add_payment(cr, uid, order_id, self._payment_fields(cr, uid, payments[2], context=context), context=context) - - session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) - if session.sequence_number <= order['sequence_number']: - session.write({'sequence_number': order['sequence_number'] + 1}) - session.refresh() - - if order['amount_return']: - cash_journal = session.cash_journal_id - if not cash_journal: - cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids) - if not len(cash_journal_ids): - raise UserError(_("No cash statement found for this session. Unable to record returned cash.")) - cash_journal = cash_journal_ids[0].journal_id - self.add_payment(cr, uid, order_id, { - 'amount': -order['amount_return'], - 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'), - 'payment_name': _('return'), - 'journal': cash_journal.id, - }, context=context) + order_id = self._process_order(cr, uid, order, context=context) order_ids.append(order_id) try: @@ -1097,7 +1101,7 @@ class pos_order(osv.osv): }) if data_type == 'product': - key = ('product', values['partner_id'], (values['product_id'], values['name']), values['debit'] > 0) + key = ('product', values['partner_id'], (values['product_id'], values['name']), values['analytic_account_id'], values['debit'] > 0) elif data_type == 'tax': key = ('tax', values['partner_id'], values['tax_code_id'], values['debit'] > 0) elif data_type == 'counter_part': @@ -1371,15 +1375,14 @@ class pos_category(osv.osv): ] def name_get(self, cr, uid, ids, context=None): - if not len(ids): - return [] - reads = self.read(cr, uid, ids, ['name','parent_id'], context=context) res = [] - for record in reads: - name = record['name'] - if record['parent_id']: - name = record['parent_id'][1]+' / '+name - res.append((record['id'], name)) + for cat in self.browse(cr, uid, ids, context=context): + names = [cat.name] + pcat = cat.parent_id + while pcat: + names.append(pcat.name) + pcat = pcat.parent_id + res.append((cat.id, ' / '.join(reversed(names)))) return res def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): @@ -1442,6 +1445,14 @@ class product_template(osv.osv): 'available_in_pos': True, } + def unlink(self, cr, uid, ids, context=None): + product_ctx = dict(context or {}, active_test=False) + if self.search_count(cr, uid, [('id', 'in', ids), ('available_in_pos', '=', True)], context=product_ctx): + if self.pool['pos.session'].search_count(cr, uid, [('state', '!=', 'closed')], context=context): + raise osv.except_osv(_('Error!'), + _('You cannot delete a product saleable in point of sale while a session is still opened.')) + return super(product_template, self).unlink(cr, uid, ids, context=context) + class res_partner(osv.osv): _inherit = 'res.partner' diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 53e22be6202..7b1a44c2ffb 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -686,7 +686,7 @@ <field name="currency_id" invisible="1"/> </group> <separator string="Available Payment Methods" colspan="4"/> - <field name="journal_ids" colspan="4" nolabel="1"> + <field name="journal_ids" colspan="4" nolabel="1" context="{'default_journal_user': 1}"> <tree string="Journals"> <field name="code" /> <field name="name" /> diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv index 4d831c7d9b1..4bf5a5ed3a5 100644 --- a/addons/point_of_sale/security/ir.model.access.csv +++ b/addons/point_of_sale/security/ir.model.access.csv @@ -37,7 +37,7 @@ access_product_template_pos_manager,product.template pos manager,product.model_p access_account_move_line,account.move.line,account.model_account_move_line,group_pos_user,1,1,1,0
access_account_move_line_manager,account.move.line manager,account.model_account_move_line,group_pos_manager,1,1,1,1
access_account_move,account.move,account.model_account_move,group_pos_manager,1,0,0,0
-access_account_journal,account.journal,account.model_account_journal,group_pos_user,1,1,1,0
+access_account_journal,account.journal,account.model_account_journal,group_pos_manager,1,1,1,0
access_account_journal_period_user,account.journal.period user,account.model_account_journal_period,group_pos_user,1,1,1,1
access_account_journal_period_manager,account.journal.period manager,account.model_account_journal_period,group_pos_manager,1,0,0,0
access_account_analytic_line,account.analytic.line,analytic.model_account_analytic_line,group_pos_user,1,1,1,0
@@ -54,7 +54,8 @@ access_res_partner_manager,res.partner manager,base.model_res_partner,group_pos_ access_product_category_manager,product.category manager,product.model_product_category,group_pos_manager,1,1,1,1
access_product_pricelist_manager,product.pricelist manager,product.model_product_pricelist,group_pos_manager,1,0,0,0
access_pos_session_user,pos.session user,model_pos_session,group_pos_user,1,1,1,0
-access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,1,1,0
+access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,0,0,0
+access_pos_config_manager,pos.config user,model_pos_config,group_pos_manager,1,1,1,0
access_ir_sequence_manager,ir.sequence manager,base.model_ir_sequence,group_pos_manager,1,1,1,1
access_product_category_pos_manager,pos.category manager,model_pos_category,group_pos_manager,1,1,1,1
access_product_category_pos_user,pos.category user,model_pos_category,group_pos_user,1,0,0,0
diff --git a/addons/portal/tests/__init__.py b/addons/portal/tests/__init__.py index 928719d5ef6..c2e8b115a09 100644 --- a/addons/portal/tests/__init__.py +++ b/addons/portal/tests/__init__.py @@ -20,6 +20,3 @@ ############################################################################## from . import test_portal -checks = [ - test_portal, -] diff --git a/addons/portal_project/tests/__init__.py b/addons/portal_project/tests/__init__.py index 77bc10416a0..4f3637eece0 100644 --- a/addons/portal_project/tests/__init__.py +++ b/addons/portal_project/tests/__init__.py @@ -21,6 +21,3 @@ from . import test_access_rights -checks = [ - test_access_rights, -] diff --git a/addons/procurement/__openerp__.py b/addons/procurement/__openerp__.py index fa3b58486b5..80232385300 100644 --- a/addons/procurement/__openerp__.py +++ b/addons/procurement/__openerp__.py @@ -60,5 +60,4 @@ Procurements in exception should be checked manually and can be re-run. 'test': ['test/procurement.yml'], 'installable': True, 'auto_install': True, - 'images': ['images/compute_schedulers.jpeg','images/config_companies_sched.jpeg', 'images/minimum_stock_rules.jpeg'], } diff --git a/addons/product/__openerp__.py b/addons/product/__openerp__.py index cfff35732be..16124d1eff3 100644 --- a/addons/product/__openerp__.py +++ b/addons/product/__openerp__.py @@ -68,5 +68,4 @@ Print product labels with barcode. ], 'installable': True, 'auto_install': False, - 'images': ['images/product_uom.jpeg','images/product_pricelists.jpeg','images/products_categories.jpeg', 'images/products_form.jpeg'], } diff --git a/addons/product/product.py b/addons/product/product.py index c3e340618c1..14480db2b4b 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -504,7 +504,8 @@ class product_template(osv.osv): 'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price."), 'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')), 'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'), - help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.", + help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders. " + "Expressed in the default unit of measure of the product.", groups="base.group_user", string="Cost Price"), 'volume': fields.float('Volume', help="The volume in m3."), 'weight': fields.float('Gross Weight', digits_compute=dp.get_precision('Stock Weight'), help="The gross weight in Kg."), @@ -1027,7 +1028,9 @@ class product_product(osv.osv): sellers = filter(lambda x: x.name.id in partner_ids, product.seller_ids) if sellers: for s in sellers: - seller_variant = s.product_name and "%s (%s)" % (s.product_name, variant) or False + seller_variant = s.product_name and ( + variant and "%s (%s)" % (s.product_name, variant) or s.product_name + ) or False mydict = { 'id': product.id, 'name': seller_variant or name, diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index a62c47a9616..0b01f805f1d 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -260,7 +260,7 @@ <p class="oe_grey"> <strong>Warning</strong>: adding or deleting attributes will delete and recreate existing variants and lead - to the loss of its eventual customizations. + to the loss of their possible customizations. </p> <field name="attribute_line_ids" widget="one2many_list" context="{'show_attribute': False}"> <tree string="Variants" editable="bottom"> diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py index 4a1ce43351b..7c05b1bf791 100644 --- a/addons/product/tests/__init__.py +++ b/addons/product/tests/__init__.py @@ -1,6 +1 @@ from . import test_uom, test_pricelist - -fast_suite = [ - test_uom, - test_pricelist -] diff --git a/addons/product_email_template/data/product_demo.xml b/addons/product_email_template/data/product_demo.xml index 2ce56226ed7..ccb880d512f 100644 --- a/addons/product_email_template/data/product_demo.xml +++ b/addons/product_email_template/data/product_demo.xml @@ -620,7 +620,7 @@ </tr> <tr> <td> - It would be very difficult to cover all of the possibilities of OpenERP and I believe the major aspects were covered very well + It would be very difficult to cover all of the possibilities of Odoo and I believe the major aspects were covered very well </td> <td> The trainer has a very good knowledge of the subject, he understands the issues of the trainees very fast and provided answers with the right level of illustration. diff --git a/addons/product_expiry/__openerp__.py b/addons/product_expiry/__openerp__.py index 5943abc5563..4f8b8a5b684 100644 --- a/addons/product_expiry/__openerp__.py +++ b/addons/product_expiry/__openerp__.py @@ -41,5 +41,4 @@ Also implements the removal strategy First Expiry First Out (FEFO) widely used, 'data' : ['product_expiry_view.xml', 'product_expiry_data.xml'], 'auto_install': False, 'installable': True, - 'images': ['images/production_lots_dates.jpeg','images/products_dates.jpeg'], } diff --git a/addons/product_margin/__openerp__.py b/addons/product_margin/__openerp__.py index 804b48d7bf7..3850f754bcc 100644 --- a/addons/product_margin/__openerp__.py +++ b/addons/product_margin/__openerp__.py @@ -42,5 +42,4 @@ The wizard to launch the report has several options to help you get the data you 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/open_margins.jpeg','images/product_margins_form.jpeg', 'images/product_margins_list.jpeg'], } diff --git a/addons/product_visible_discount/__openerp__.py b/addons/product_visible_discount/__openerp__.py index 33151a609ab..df48ad14202 100644 --- a/addons/product_visible_discount/__openerp__.py +++ b/addons/product_visible_discount/__openerp__.py @@ -41,5 +41,4 @@ To this end, a new check box named 'Visible Discount' is added to the pricelist 'data': ['product_visible_discount_view.xml'], 'auto_install': False, 'installable': True, - 'images': ['images/pricelists_visible_discount.jpeg'], } diff --git a/addons/project/__openerp__.py b/addons/project/__openerp__.py index 67307ca90ea..e20101b371b 100644 --- a/addons/project/__openerp__.py +++ b/addons/project/__openerp__.py @@ -27,16 +27,6 @@ 'category': 'Project Management', 'sequence': 8, 'summary': 'Projects, Tasks', - 'images': [ - 'images/gantt.png', - 'images/project_task_tree.jpeg', - 'images/project_task.jpeg', - 'images/project.jpeg', - 'images/task_analysis.jpeg', - 'images/project_kanban.jpeg', - 'images/task_kanban.jpeg', - 'images/task_stages.jpeg' - ], 'depends': [ 'base_setup', 'product', diff --git a/addons/project/tests/__init__.py b/addons/project/tests/__init__.py index 2a0efe3614b..ac890eeab6d 100644 --- a/addons/project/tests/__init__.py +++ b/addons/project/tests/__init__.py @@ -21,6 +21,3 @@ from . import test_project_flow -checks = [ - test_project_flow, -] diff --git a/addons/project_issue/__openerp__.py b/addons/project_issue/__openerp__.py index d0a1d48476f..78a93ef877d 100644 --- a/addons/project_issue/__openerp__.py +++ b/addons/project_issue/__openerp__.py @@ -35,7 +35,6 @@ It allows the manager to quickly check the issues, assign them and decide on the """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/project-management', - 'images': ['images/issue_analysis.jpeg','images/project_issue.jpeg'], 'depends': [ 'sales_team', 'project', diff --git a/addons/project_issue_sheet/__openerp__.py b/addons/project_issue_sheet/__openerp__.py index 5a82e5692dc..eb264d865b3 100644 --- a/addons/project_issue_sheet/__openerp__.py +++ b/addons/project_issue_sheet/__openerp__.py @@ -32,7 +32,6 @@ Worklogs can be maintained to signify number of hours spent by users to handle a """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/project-management', - 'images': ['images/project_issue_sheet_worklog.jpeg'], 'depends': [ 'project_issue', 'hr_timesheet_sheet', diff --git a/addons/project_timesheet/__openerp__.py b/addons/project_timesheet/__openerp__.py index 383872955d4..4327d7a1871 100644 --- a/addons/project_timesheet/__openerp__.py +++ b/addons/project_timesheet/__openerp__.py @@ -33,7 +33,6 @@ with the effect of creating, editing and deleting either ways. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/project-management', - 'images': ['images/invoice_task_work.jpeg', 'images/my_timesheet.jpeg', 'images/working_hour.jpeg'], 'depends': ['resource', 'project', 'hr_timesheet', 'hr_timesheet_invoice', 'account_analytic_analysis', 'procurement'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/purchase/__openerp__.py b/addons/purchase/__openerp__.py index bc64adfadd6..2740000bf44 100644 --- a/addons/purchase/__openerp__.py +++ b/addons/purchase/__openerp__.py @@ -45,7 +45,6 @@ Dashboard / Reports for Purchase Management will include: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/purchase', - 'images': ['images/purchase_order.jpeg', 'images/purchase_analysis.jpeg', 'images/request_for_quotation.jpeg'], 'depends': ['stock_account', 'report'], 'data': [ 'security/purchase_security.xml', diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 33badfa98b4..088688fbb97 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -61,15 +61,14 @@ class purchase_order(osv.osv): if not value: return False if type(ids)!=type([]): ids=[ids] + pol_obj = self.pool.get('purchase.order.line') for po in self.browse(cr, uid, ids, context=context): if po.order_line: - cr.execute("""update purchase_order_line set - date_planned=%s - where - order_id=%s and - (date_planned=%s or date_planned<%s)""", (value,po.id,po.minimum_planned_date,value)) - cr.execute("""update purchase_order set - minimum_planned_date=%s where id=%s""", (value, po.id)) + pol_ids = pol_obj.search(cr, uid, [ + ('order_id', '=', po.id), '|', ('date_planned', '=', po.minimum_planned_date), ('date_planned', '<', value) + ], context=context) + pol_obj.write(cr, uid, pol_ids, {'date_planned': value}, context=context) + self.pool.get('purchase.order').write(cr, uid, po.id, {'minimum_planned_date': value}, context=context) self.invalidate_cache(cr, uid, context=context) return True @@ -715,7 +714,7 @@ class purchase_order(osv.osv): 'location_id': order.partner_id.property_stock_supplier.id, 'location_dest_id': order.location_id.id, 'picking_id': picking_id, - 'partner_id': order.dest_address_id.id or order.partner_id.id, + 'partner_id': order.dest_address_id.id, 'move_dest_id': False, 'state': 'draft', 'purchase_line_id': order_line.id, @@ -823,7 +822,7 @@ class purchase_order(osv.osv): for order in self.browse(cr, uid, ids): picking_vals = { 'picking_type_id': order.picking_type_id.id, - 'partner_id': order.dest_address_id.id or order.partner_id.id, + 'partner_id': order.partner_id.id, 'date': max([l.date_planned for l in order.order_line]), 'origin': order.name } diff --git a/addons/purchase_analytic_plans/__openerp__.py b/addons/purchase_analytic_plans/__openerp__.py index d29fda9fcd0..5d93127fd8d 100644 --- a/addons/purchase_analytic_plans/__openerp__.py +++ b/addons/purchase_analytic_plans/__openerp__.py @@ -33,7 +33,6 @@ on a supplier purchase order into several accounts and analytic plans. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/purchase', - 'images': ['images/purchase_analytic_distribution.jpeg'], 'depends': ['purchase', 'account_analytic_plans'], 'data': ['purchase_analytic_plans_view.xml'], 'demo': [], diff --git a/addons/purchase_double_validation/__openerp__.py b/addons/purchase_double_validation/__openerp__.py index d1c0caa394b..faa02ff4854 100644 --- a/addons/purchase_double_validation/__openerp__.py +++ b/addons/purchase_double_validation/__openerp__.py @@ -23,7 +23,6 @@ 'name' : 'Double Validation on Purchases', 'version' : '1.1', 'category': 'Purchase Management', - 'images' : ['images/purchase_validation.jpeg'], 'depends' : ['base','purchase'], 'author' : 'OpenERP SA', 'description': """ diff --git a/addons/purchase_requisition/__openerp__.py b/addons/purchase_requisition/__openerp__.py index 61e7195d528..85ead748637 100644 --- a/addons/purchase_requisition/__openerp__.py +++ b/addons/purchase_requisition/__openerp__.py @@ -22,7 +22,6 @@ 'version': '0.1', 'author': 'OpenERP SA', 'category': 'Purchase Management', - 'images': ['images/purchase_requisitions.jpeg'], 'website': 'https://www.odoo.com/page/purchase', 'description': """ This module allows you to manage your Purchase Requisition. diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py index 7fd33c367d9..277b3945798 100644 --- a/addons/purchase_requisition/purchase_requisition.py +++ b/addons/purchase_requisition/purchase_requisition.py @@ -303,7 +303,7 @@ class purchase_requisition_line(osv.osv): _rec_name = 'product_id' _columns = { - 'product_id': fields.many2one('product.product', 'Product'), + 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok', '=', True)]), 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'), 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure')), 'requisition_id': fields.many2one('purchase.requisition', 'Call for Tenders', ondelete='cascade'), diff --git a/addons/report/static/src/js/qwebactionmanager.js b/addons/report/static/src/js/qwebactionmanager.js index 088c40e800a..74de70d03dd 100644 --- a/addons/report/static/src/js/qwebactionmanager.js +++ b/addons/report/static/src/js/qwebactionmanager.js @@ -1,14 +1,19 @@ openerp.report = function(instance) { var wkhtmltopdf_state; - var trigger_download = function(session, response, c) { + var trigger_download = function(session, response, c, action, options) { session.get_file({ url: '/report/download', data: {data: JSON.stringify(response)}, complete: openerp.web.unblockUI, - error: c.rpc_error.bind(c) + error: c.rpc_error.bind(c), + success: function(){ + if (action && options && !action.dialog) { + options.on_close(); + } + }, }); - } + }; instance.web.ActionManager = instance.web.ActionManager.extend({ ir_actions_report_xml: function(action, options) { @@ -79,10 +84,10 @@ workers to print a pdf version of the reports.'), true); support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \ target="_blank">wkhtmltopdf.org</a>'), true); } - return trigger_download(self.session, response, c); + return trigger_download(self.session, response, c, action, options); }); } else if (action.report_type === 'controller') { - return trigger_download(self.session, response, c); + return trigger_download(self.session, response, c, action, options); } } else { return self._super(action, options); diff --git a/addons/report_intrastat/__openerp__.py b/addons/report_intrastat/__openerp__.py index 3f14f096e99..be4b75282e4 100644 --- a/addons/report_intrastat/__openerp__.py +++ b/addons/report_intrastat/__openerp__.py @@ -43,5 +43,4 @@ European Union.""", 'test': ['test/report_intrastat_report.yml'], 'installable': True, 'auto_install': False, - 'images': ['images/country_intrastat_code.jpeg','images/intrastat_code.jpeg'], } diff --git a/addons/report_webkit/__openerp__.py b/addons/report_webkit/__openerp__.py index 1c6d34c5aa5..923006965eb 100644 --- a/addons/report_webkit/__openerp__.py +++ b/addons/report_webkit/__openerp__.py @@ -98,5 +98,4 @@ TODO: ], 'installable': True, 'auto_install': False, - 'images': ['images/companies_webkit.jpeg','images/header_html.jpeg','images/header_img.jpeg'], } diff --git a/addons/resource/__openerp__.py b/addons/resource/__openerp__.py index 3ed86a595c8..e60453e7723 100644 --- a/addons/resource/__openerp__.py +++ b/addons/resource/__openerp__.py @@ -48,5 +48,4 @@ associated to every resource. It also manages the leaves of every resource. ], 'installable': True, 'auto_install': False, - 'images': ['images/resource_leaves_calendar.jpeg','images/resource_leaves_form.jpeg'], } diff --git a/addons/resource/tests/__init__.py b/addons/resource/tests/__init__.py index 55e60eb7930..7c4e0b0887a 100644 --- a/addons/resource/tests/__init__.py +++ b/addons/resource/tests/__init__.py @@ -19,8 +19,5 @@ # ############################################################################## -from openerp.addons.resource.tests import test_resource +from . import test_resource -checks = [ - test_resource, -] diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index afc84eccc6d..f11263eb5d6 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -58,7 +58,6 @@ The Dashboard for the Sales Manager will include """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/crm', - 'images': ['images/Sale_order_line_to_invoice.jpeg','images/sale_order.jpeg','images/sales_analysis.jpeg'], 'depends': ['sales_team','account_voucher', 'procurement', 'report'], 'data': [ 'wizard/sale_make_invoice_advance.xml', diff --git a/addons/sale/wizard/sale_line_invoice.py b/addons/sale/wizard/sale_line_invoice.py index 3b81374c721..1ac1c41a9cf 100644 --- a/addons/sale/wizard/sale_line_invoice.py +++ b/addons/sale/wizard/sale_line_invoice.py @@ -27,6 +27,31 @@ from openerp.exceptions import UserError class sale_order_line_make_invoice(osv.osv_memory): _name = "sale.order.line.make.invoice" _description = "Sale OrderLine Make_invoice" + + def _prepare_invoice(self, cr, uid, order, lines, context=None): + a = order.partner_id.property_account_receivable.id + if order.partner_id and order.partner_id.property_payment_term.id: + pay_term = order.partner_id.property_payment_term.id + else: + pay_term = False + return { + 'name': order.client_order_ref or '', + 'origin': order.name, + 'type': 'out_invoice', + 'reference': "P%dSO%d" % (order.partner_id.id, order.id), + 'account_id': a, + 'partner_id': order.partner_invoice_id.id, + 'invoice_line': [(6, 0, lines)], + 'currency_id' : order.pricelist_id.currency_id.id, + 'comment': order.note, + 'payment_term': pay_term, + 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id, + 'user_id': order.user_id and order.user_id.id or False, + 'company_id': order.company_id and order.company_id.id or False, + 'date_invoice': fields.date.today(), + 'section_id': order.section_id.id, + } + def make_invoices(self, cr, uid, ids, context=None): """ @@ -56,27 +81,7 @@ class sale_order_line_make_invoice(osv.osv_memory): @return: """ - a = order.partner_id.property_account_receivable.id - if order.partner_id and order.partner_id.property_payment_term.id: - pay_term = order.partner_id.property_payment_term.id - else: - pay_term = False - inv = { - 'name': order.client_order_ref or '', - 'origin': order.name, - 'type': 'out_invoice', - 'reference': "P%dSO%d" % (order.partner_id.id, order.id), - 'account_id': a, - 'partner_id': order.partner_invoice_id.id, - 'invoice_line': [(6, 0, lines)], - 'currency_id' : order.pricelist_id.currency_id.id, - 'comment': order.note, - 'payment_term': pay_term, - 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id, - 'user_id': order.user_id and order.user_id.id or False, - 'company_id': order.company_id and order.company_id.id or False, - 'date_invoice': fields.date.today(), - } + inv = self._prepare_invoice(cr, uid, order, lines) inv_id = self.pool.get('account.invoice').create(cr, uid, inv) return inv_id diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index c62f6eb3aa6..dcd92e48d4d 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -145,7 +145,8 @@ class sale_advance_payment_inv(osv.osv_memory): 'currency_id': sale.pricelist_id.currency_id.id, 'comment': '', 'payment_term': sale.payment_term.id, - 'fiscal_position': sale.fiscal_position.id or sale.partner_id.property_account_position.id + 'fiscal_position': sale.fiscal_position.id or sale.partner_id.property_account_position.id, + 'section_id': sale.section_id.id, } result.append((sale.id, inv_values)) return result diff --git a/addons/sale_analytic_plans/__openerp__.py b/addons/sale_analytic_plans/__openerp__.py index c9276d0935a..44e60c6e2da 100644 --- a/addons/sale_analytic_plans/__openerp__.py +++ b/addons/sale_analytic_plans/__openerp__.py @@ -31,7 +31,6 @@ Using this module you will be able to link analytic accounts to sales orders. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/crm', - 'images': ['images/sale_order_analytic_account.jpeg', 'images/sales_order_line.jpeg'], 'depends': ['sale', 'account_analytic_plans'], 'data': ['sale_analytic_plans_view.xml'], 'demo': [], diff --git a/addons/sale_crm/__openerp__.py b/addons/sale_crm/__openerp__.py index 90d89bb4758..c13509b8a3a 100644 --- a/addons/sale_crm/__openerp__.py +++ b/addons/sale_crm/__openerp__.py @@ -36,7 +36,6 @@ modules. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/crm', - 'images': ['images/crm_statistics_dashboard.jpeg', 'images/opportunity_to_quote.jpeg'], 'depends': ['sale', 'crm', 'web_kanban_gauge'], 'data': [ 'wizard/crm_make_sale_view.xml', diff --git a/addons/sale_journal/__openerp__.py b/addons/sale_journal/__openerp__.py index 07a6b53925e..68d8966de86 100644 --- a/addons/sale_journal/__openerp__.py +++ b/addons/sale_journal/__openerp__.py @@ -50,7 +50,6 @@ Some statistics by journals are provided. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/billing', - 'images': ['images/invoice_type.jpeg'], 'depends': ['sale_stock'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/sale_margin/__openerp__.py b/addons/sale_margin/__openerp__.py index 81c7fdb499c..5125b9b8c4f 100644 --- a/addons/sale_margin/__openerp__.py +++ b/addons/sale_margin/__openerp__.py @@ -30,7 +30,6 @@ This gives the profitability by calculating the difference between the Unit Price and Cost Price. """, 'author':'OpenERP SA', - 'images':['images/sale_margin.jpeg'], 'depends':['sale'], 'demo':['sale_margin_demo.xml'], 'test': ['test/sale_margin.yml'], diff --git a/addons/sale_mrp/__openerp__.py b/addons/sale_mrp/__openerp__.py index bd5b1caa1ea..6fe723427fb 100644 --- a/addons/sale_mrp/__openerp__.py +++ b/addons/sale_mrp/__openerp__.py @@ -33,7 +33,6 @@ from sales order. It adds sales name and sales Reference on production order. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/manufacturing', - 'images': ['images/SO_to_MO.jpeg'], 'depends': ['mrp', 'sale_stock'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/sale_mrp/tests/__init__.py b/addons/sale_mrp/tests/__init__.py index 45d5119d43c..7ed715e54fb 100644 --- a/addons/sale_mrp/tests/__init__.py +++ b/addons/sale_mrp/tests/__init__.py @@ -21,6 +21,3 @@ from . import test_move_explode -checks = [ - test_move_explode, -] diff --git a/addons/sale_order_dates/__openerp__.py b/addons/sale_order_dates/__openerp__.py index 6edc18142d1..d309900f19d 100644 --- a/addons/sale_order_dates/__openerp__.py +++ b/addons/sale_order_dates/__openerp__.py @@ -36,7 +36,6 @@ You can add the following additional dates to a sales order: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/crm', - 'images': ['images/sale_order_dates.jpeg'], 'depends': ['sale_stock'], 'data': ['sale_order_dates_view.xml'], 'demo': [], diff --git a/addons/sale_service/__openerp__.py b/addons/sale_service/__openerp__.py index 94598e37636..8f02026361b 100644 --- a/addons/sale_service/__openerp__.py +++ b/addons/sale_service/__openerp__.py @@ -47,7 +47,6 @@ completed. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/crm', - 'images': ['images/product.jpeg', 'images/task_from_SO.jpeg'], 'depends': ['project', 'procurement', 'sale', 'procurement_jit'], 'data': ['views/sale_service_view.xml'], 'demo': ['demo/sale_service_demo.xml'], diff --git a/addons/sale_stock/__openerp__.py b/addons/sale_stock/__openerp__.py index 7d0200319ef..a7c68b4cb2a 100644 --- a/addons/sale_stock/__openerp__.py +++ b/addons/sale_stock/__openerp__.py @@ -44,7 +44,6 @@ You can choose flexible invoicing methods: """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/warehouse', - 'images': ['images/deliveries_to_invoice.jpeg'], 'depends': ['sale', 'stock_account'], 'data': [ 'security/sale_stock_security.xml', diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index f7cd516d004..683b2ff47db 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -377,7 +377,7 @@ class stock_move(osv.osv): return invoice_line_id def _get_master_data(self, cr, uid, move, company, context=None): - if move.procurement_id and move.procurement_id.sale_line_id: + if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.order_policy == 'picking': sale_order = move.procurement_id.sale_line_id.order_id return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id return super(stock_move, self)._get_master_data(cr, uid, move, company, context=context) @@ -417,7 +417,7 @@ class stock_picking(osv.osv): """ saleorder_ids = self.pool['sale.order'].search(cr, uid, [('procurement_group_id' ,'=', picking.group_id.id)], context=context) saleorders = self.pool['sale.order'].browse(cr, uid, saleorder_ids, context=context) - if saleorders and saleorders[0]: + if saleorders and saleorders[0] and saleorders[0].order_policy == 'picking': saleorder = saleorders[0] return saleorder.partner_invoice_id.id return super(stock_picking, self)._get_partner_to_invoice(cr, uid, picking, context=context) diff --git a/addons/share/__openerp__.py b/addons/share/__openerp__.py index 87daf1fa36d..0354085d873 100644 --- a/addons/share/__openerp__.py +++ b/addons/share/__openerp__.py @@ -52,5 +52,4 @@ synchronization with other companies. 'auto_install': True, 'web': True, 'qweb' : ['static/src/xml/*.xml'], - 'images': ['images/share_wizard.jpeg','images/sharing_wizard_step1.jpeg', 'images/sharing_wizard_step2.jpeg'], } diff --git a/addons/share/wizard/share_wizard.py b/addons/share/wizard/share_wizard.py index 161cbfba112..3d6246c5eff 100644 --- a/addons/share/wizard/share_wizard.py +++ b/addons/share/wizard/share_wizard.py @@ -831,7 +831,7 @@ class share_wizard(osv.TransientModel): body += '%s\n\n' % ((user.signature or '')) body += "--\n" body += _("Odoo is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n" - "It is open source and can be found on http://www.openerp.com.") + "It is open source and can be found on https://www.odoo.com.") msg_id = message_obj.schedule_with_attach(cr, uid, user.email, [email_to], subject, body, model='', context=context) notification_obj.create(cr, uid, {'user_id': result_line.user_id.id, 'message_id': msg_id}, context=context) @@ -865,7 +865,7 @@ class share_wizard(osv.TransientModel): body += "\n\n%s\n\n" % ( (user.signature or '') ) body += "--\n" body += _("Odoo is a powerful and user-friendly suite of Business Applications (CRM, Sales, HR, etc.)\n" - "It is open source and can be found on http://www.openerp.com.") + "It is open source and can be found on https://www.odoo.com.") mail_ids.append(mail_mail.create(cr, uid, { 'email_from': user.email, 'email_to': email_to, diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 215a160a3ba..cd39d7ec2cf 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -51,7 +51,6 @@ Dashboard / Reports for Warehouse Management will include: * Moves Analysis """, 'website': 'https://www.odoo.com/page/warehouse', - 'images': ['images/stock_forecast_report.png', 'images/delivery_orders.jpeg', 'images/inventory_analysis.jpeg','images/location.jpeg','images/moves_analysis.jpeg','images/physical_inventories.jpeg'], 'depends': ['product', 'procurement', 'board', 'web_kanban_gauge', 'web_kanban_sparkline', 'barcodes'], 'category': 'Warehouse Management', 'sequence': 16, diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml index 6a47664d2ab..23a3988871a 100644 --- a/addons/stock/product_view.xml +++ b/addons/stock/product_view.xml @@ -23,7 +23,7 @@ <field name="inherit_id" ref="product.product_template_tree_view"/> <field name="arch" type="xml"> <tree position="attributes"> - <attribute name="colors">{'red':virtual_available<0, 'blue':virtual_available>=0 and state in ('draft', 'end', 'obsolete'), 'black':virtual_available>=0 and state not in ('draft', 'end', 'obsolete')}</attribute> + <attribute name="colors">red:virtual_available<0;blue:virtual_available>=0 and state in ('draft', 'end', 'obsolete');black:virtual_available>=0 and state not in ('draft', 'end', 'obsolete')</attribute> </tree> <field name="uom_id" position="after"> <field name="qty_available"/> diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index cbe1d652e4d..4bb2c1cbf52 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -68,8 +68,10 @@ access_stock_pack_operation_manager,stock.pack.operation manager,model_stock_pac access_stock_pack_operation_user,stock.pack.operation user,model_stock_pack_operation,stock.group_stock_user,1,1,1,1 access_stock_pack_operation_all,stock.pack.operation all users,model_stock_pack_operation,base.group_user,1,0,0,0 access_product_putaway_all,product.putaway all users,model_product_putaway,base.group_user,1,0,0,0 +access_product_putaway_manager,product.putaway all managers,model_product_putaway,stock.group_stock_manager,1,1,1,1 access_stock_removal_all,product.removal all users,model_product_removal,base.group_user,1,0,0,0 access_stock_fixed_putaway_strat,stock_fixed_putaway_strat managers,model_stock_fixed_putaway_strat,stock.group_stock_manager,1,1,1,1 +access_stock_fixed_putaway_user,stock_fixed_putaway_strat user,model_stock_fixed_putaway_strat,stock.group_stock_user,1,0,0,0 access_stock_move_operation_link_manager,stock.move.operation.link manager,model_stock_move_operation_link,stock.group_stock_manager,1,1,1,1 access_stock_move_operation_link_user,stock.move.operation.link user,model_stock_move_operation_link,stock.group_stock_user,1,1,1,1 access_stock_move_operation_link_all,stock.move.operation.link all users,model_stock_move_operation_link,base.group_user,1,0,0,0 diff --git a/addons/stock/stock.py b/addons/stock/stock.py index cff0da06742..df592b5285f 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1061,6 +1061,7 @@ class stock_picking(osv.osv): 'product_qty': 1.0, 'location_id': pack.location_id.id, 'location_dest_id': quants_suggested_locations[pack_quants[0]], + 'owner_id': picking.owner_id.id, }) #remove the quants inside the package so that they are excluded from the rest of the computation for quant in pack_quants: @@ -1079,7 +1080,7 @@ class stock_picking(osv.osv): if qty <= 0: continue suggested_location_id = _picking_putaway_apply(product) - key = (product.id, False, False, False, picking.location_id.id, suggested_location_id) + key = (product.id, False, False, picking.owner_id.id, picking.location_id.id, suggested_location_id) if qtys_grouped.get(key): qtys_grouped[key] += qty else: @@ -1735,6 +1736,7 @@ class stock_move(osv.osv): 'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]}), 'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}), 'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}), + 'product_tmpl_id': fields.related('product_id', 'product_tmpl_id', type='many2one', relation='product.template', string='Product Template'), 'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."), @@ -1813,8 +1815,7 @@ class stock_move(osv.osv): return False def _default_destination_address(self, cr, uid, context=None): - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - return user.company_id.partner_id.id + return False _defaults = { 'location_id': _default_location_source, @@ -2975,7 +2976,7 @@ class stock_warehouse(osv.osv): inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context) inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context) values = [(output_loc, transit_location, wh.out_type_id.id, wh), (transit_location, input_loc, warehouse.in_type_id.id, warehouse)] - pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context) + pull_rules_list = self._get_supply_pull_rules(cr, uid, wh.id, values, inter_wh_route_id, context=context) for pull_rule in pull_rules_list: pull_obj.create(cr, uid, vals=pull_rule, context=context) #if the warehouse is also set as default resupply method, assign this route automatically to the warehouse @@ -3036,7 +3037,7 @@ class stock_warehouse(osv.osv): 'sequence': 10, } - def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None): + def _get_supply_pull_rules(self, cr, uid, supply_warehouse, values, new_route_id, context=None): pull_rules_list = [] for from_loc, dest_loc, pick_type_id, warehouse in values: pull_rules_list.append({ @@ -3047,8 +3048,8 @@ class stock_warehouse(osv.osv): 'action': 'move', 'picking_type_id': pick_type_id, 'procure_method': warehouse.lot_stock_id.id != from_loc.id and 'make_to_order' or 'make_to_stock', # first part of the resuply route is MTS - 'warehouse_id': supplied_warehouse.id, - 'propagate_warehouse_id': warehouse.id, + 'warehouse_id': warehouse.id, + 'propagate_warehouse_id': supply_warehouse, }) return pull_rules_list diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index 7d82c4d65be..477d2d47489 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -288,7 +288,7 @@ <field name="product_id"/> <field name="product_uom_qty"/> <field name="product_uom" string="Unit of Measure" groups="product.group_uom"/> - <field name="product_packaging" domain="[('product_tmpl_id','=',product_id)]" groups="product.group_stock_packaging"/> + <field name="product_packaging" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging"/> <field name="picking_id"/> <field name="location_id" groups="stock.group_locations"/> <field name="location_dest_id" groups="stock.group_locations"/> @@ -1111,7 +1111,8 @@ <field name="product_uos" on_change="onchange_quantity(product_id, product_uom_qty, product_uom, product_uos)" class="oe_inline"/> </div> <field name="name"/> - <field name="product_packaging" groups="product.group_stock_packaging" domain="[('product_tmpl_id','=',product_id)]"/> + <field name="product_tmpl_id" invisible="1"/> + <field name="product_packaging" groups="product.group_stock_packaging" domain="[('product_tmpl_id','=',product_tmpl_id)]"/> </group> <group> <field name="create_date" invisible="1"/> diff --git a/addons/stock/tests/__init__.py b/addons/stock/tests/__init__.py new file mode 100644 index 00000000000..9d14a332a28 --- /dev/null +++ b/addons/stock/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_stock_flow diff --git a/addons/stock/tests/common.py b/addons/stock/tests/common.py new file mode 100644 index 00000000000..432c1e57f88 --- /dev/null +++ b/addons/stock/tests/common.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +from openerp.tests import common + + +class TestStockCommon(common.TransactionCase): + + def setUp(self): + super(TestStockCommon, self).setUp() + + self.ProductObj = self.env['product.product'] + self.UomObj = self.env['product.uom'] + self.PartnerObj = self.env['res.partner'] + self.ModelDataObj = self.env['ir.model.data'] + self.StockPackObj = self.env['stock.pack.operation'] + self.StockQuantObj = self.env['stock.quant'] + self.PickingObj = self.env['stock.picking'] + self.MoveObj = self.env['stock.move'] + self.InvObj = self.env['stock.inventory'] + self.InvLineObj = self.env['stock.inventory.line'] + + # Model Data + self.partner_agrolite_id = self.ModelDataObj.xmlid_to_res_id('base.res_partner_2') + self.partner_delta_id = self.ModelDataObj.xmlid_to_res_id('base.res_partner_4') + self.picking_type_in = self.ModelDataObj.xmlid_to_res_id('stock.picking_type_in') + self.picking_type_out = self.ModelDataObj.xmlid_to_res_id('stock.picking_type_out') + self.supplier_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers') + self.stock_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock') + self.customer_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers') + self.categ_unit = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_unit') + self.categ_kgm = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_kgm') + + # Product Created A, B, C, D + self.productA = self.ProductObj.create({'name': 'Product A'}) + self.productB = self.ProductObj.create({'name': 'Product B'}) + self.productC = self.ProductObj.create({'name': 'Product C'}) + self.productD = self.ProductObj.create({'name': 'Product D'}) + + # Configure unit of measure. + self.uom_kg = self.UomObj.create({ + 'name': 'Test-KG', + 'category_id': self.categ_kgm, + 'factor_inv': 1, + 'factor': 1, + 'uom_type': 'reference', + 'rounding': 0.000001}) + self.uom_tone = self.UomObj.create({ + 'name': 'Test-Tone', + 'category_id': self.categ_kgm, + 'uom_type': 'bigger', + 'factor_inv': 1000.0, + 'rounding': 0.001}) + self.uom_gm = self.UomObj.create({ + 'name': 'Test-G', + 'category_id': self.categ_kgm, + 'uom_type': 'smaller', + 'factor': 1000.0, + 'rounding': 0.001}) + self.uom_mg = self.UomObj.create({ + 'name': 'Test-MG', + 'category_id': self.categ_kgm, + 'uom_type': 'smaller', + 'factor': 100000.0, + 'rounding': 0.001}) + # Check Unit + self.uom_unit = self.UomObj.create({ + 'name': 'Test-Unit', + 'category_id': self.categ_unit, + 'factor': 1, + 'uom_type': 'reference', + 'rounding': 1.0}) + self.uom_dozen = self.UomObj.create({ + 'name': 'Test-DozenA', + 'category_id': self.categ_unit, + 'factor_inv': 12, + 'uom_type': 'bigger', + 'rounding': 0.001}) + self.uom_sdozen = self.UomObj.create({ + 'name': 'Test-SDozenA', + 'category_id': self.categ_unit, + 'factor_inv': 144, + 'uom_type': 'bigger', + 'rounding': 0.001}) + self.uom_sdozen_round = self.UomObj.create({ + 'name': 'Test-SDozenA Round', + 'category_id': self.categ_unit, + 'factor_inv': 144, + 'uom_type': 'bigger', + 'rounding': 1.0}) + + # Product for different unit of measure. + self.DozA = self.ProductObj.create({'name': 'Dozon-A', 'uom_id': self.uom_dozen.id, 'uom_po_id': self.uom_dozen.id}) + self.SDozA = self.ProductObj.create({'name': 'SuperDozon-A', 'uom_id': self.uom_sdozen.id, 'uom_po_id': self.uom_sdozen.id}) + self.SDozARound = self.ProductObj.create({'name': 'SuperDozenRound-A', 'uom_id': self.uom_sdozen_round.id, 'uom_po_id': self.uom_sdozen_round.id}) + self.UnitA = self.ProductObj.create({'name': 'Unit-A'}) + self.kgB = self.ProductObj.create({'name': 'kg-B', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id}) + self.gB = self.ProductObj.create({'name': 'g-B', 'uom_id': self.uom_gm.id, 'uom_po_id': self.uom_gm.id}) diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py new file mode 100644 index 00000000000..32c9e5e3eb0 --- /dev/null +++ b/addons/stock/tests/test_stock_flow.py @@ -0,0 +1,1097 @@ +# -*- coding: utf-8 -*- + +from openerp.addons.stock.tests.common import TestStockCommon +from openerp.tools import mute_logger, float_round + + +class TestStockFlow(TestStockCommon): + + @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models') + def test_00_picking_create_and_transfer_quantity(self): + """ Basic stock operation on incoming and outgoing shipment. """ + LotObj = self.env['stock.production.lot'] + # ---------------------------------------------------------------------- + # Create incoming shipment of product A, B, C, D + # ---------------------------------------------------------------------- + # Product A ( 1 Unit ) , Product C ( 10 Unit ) + # Product B ( 1 Unit ) , Product D ( 10 Unit ) + # Product D ( 5 Unit ) + # ---------------------------------------------------------------------- + + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in}) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 1, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.productB.name, + 'product_id': self.productB.id, + 'product_uom_qty': 1, + 'product_uom': self.productB.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.productC.name, + 'product_id': self.productC.id, + 'product_uom_qty': 10, + 'product_uom': self.productC.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.productD.name, + 'product_id': self.productD.id, + 'product_uom_qty': 10, + 'product_uom': self.productD.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.productD.name, + 'product_id': self.productD.id, + 'product_uom_qty': 5, + 'product_uom': self.productD.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'draft', 'Wrong state of move line.') + # Confirm incoming shipment. + picking_in.action_confirm() + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + + # ---------------------------------------------------------------------- + # Replace pack operation of incoming shipments. + # ---------------------------------------------------------------------- + + picking_in.do_prepare_partial() + self.StockPackObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_in.id)]).write({ + 'product_qty': 4.0}) + self.StockPackObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_in.id)]).write({ + 'product_qty': 5.0}) + self.StockPackObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_in.id)]).write({ + 'product_qty': 5.0}) + self.StockPackObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_in.id)]).write({ + 'product_qty': 5.0}) + lot2_productC = LotObj.create({'name': 'C Lot 2', 'product_id': self.productC.id}) + self.StockPackObj.create({ + 'product_id': self.productC.id, + 'product_qty': 2, + 'product_uom_id': self.productC.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': picking_in.id, + 'lot_id': lot2_productC.id}) + self.StockPackObj.create({ + 'product_id': self.productD.id, + 'product_qty': 2, + 'product_uom_id': self.productD.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': picking_in.id}) + + # Check incoming shipment total quantity of pack operation + packs = self.StockPackObj.search([('picking_id', '=', picking_in.id)]) + total_qty = [pack.product_qty for pack in packs] + self.assertEqual(sum(total_qty), 23, 'Wrong quantity in pack operation (%s found instead of 23)' % (sum(total_qty))) + + # Transfer Incoming Shipment. + picking_in.do_transfer() + + # ---------------------------------------------------------------------- + # Check state, quantity and total moves of incoming shipment. + # ---------------------------------------------------------------------- + + # Check total no of move lines of incoming shipment. + self.assertEqual(len(picking_in.move_lines), 6, 'Wrong number of move lines.') + # Check incoming shipment state. + self.assertEqual(picking_in.state, 'done', 'Incoming shipment state should be done.') + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + # Check product A done quantity must be 3 and 1 + moves = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_in.id)]) + a_done_qty = [move.product_uom_qty for move in moves] + self.assertEqual(set(a_done_qty), set([1.0, 3.0]), 'Wrong move quantity for product A.') + # Check product B done quantity must be 4 and 1 + moves = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_in.id)]) + b_done_qty = [move.product_uom_qty for move in moves] + self.assertEqual(set(b_done_qty), set([4.0, 1.0]), 'Wrong move quantity for product B.') + # Check product C done quantity must be 7 + c_done_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_in.id)], limit=1).product_uom_qty + self.assertEqual(c_done_qty, 7.0, 'Wrong move quantity of product C (%s found instead of 7)' % (c_done_qty)) + # Check product D done quantity must be 7 + d_done_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_in.id)], limit=1).product_uom_qty + self.assertEqual(d_done_qty, 7.0, 'Wrong move quantity of product D (%s found instead of 7)' % (d_done_qty)) + + # ---------------------------------------------------------------------- + # Check Back order of Incoming shipment. + # ---------------------------------------------------------------------- + + # Check back order created or not. + back_order_in = self.PickingObj.search([('backorder_id', '=', picking_in.id)]) + self.assertEqual(len(back_order_in), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(back_order_in.move_lines), 3, 'Wrong number of move lines.') + # Check back order should be created with 3 quantity of product C. + moves = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', back_order_in.id)]) + product_c_qty = [move.product_uom_qty for move in moves] + self.assertEqual(sum(product_c_qty), 3.0, 'Wrong move quantity of product C (%s found instead of 3)' % (product_c_qty)) + # Check back order should be created with 8 quantity of product D. + moves = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_in.id)]) + product_d_qty = [move.product_uom_qty for move in moves] + self.assertEqual(sum(product_d_qty), 8.0, 'Wrong move quantity of product D (%s found instead of 8)' % (product_d_qty)) + + # ====================================================================== + # Create Outgoing shipment with ... + # product A ( 10 Unit ) , product B ( 5 Unit ) + # product C ( 3 unit ) , product D ( 10 Unit ) + # ====================================================================== + + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out}) + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 10, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.productB.name, + 'product_id': self.productB.id, + 'product_uom_qty': 5, + 'product_uom': self.productB.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.productC.name, + 'product_id': self.productC.id, + 'product_uom_qty': 3, + 'product_uom': self.productC.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.productD.name, + 'product_id': self.productD.id, + 'product_uom_qty': 10, + 'product_uom': self.productD.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + # Confirm outgoing shipment. + picking_out.action_confirm() + for move in picking_out.move_lines: + self.assertEqual(move.state, 'confirmed', 'Wrong state of move line.') + # Product assign to outgoing shipments + picking_out.action_assign() + for move in picking_out.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + # Check availability for product A + aval_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(aval_a_qty, 4.0, 'Wrong move quantity availability of product A (%s found instead of 4)' % (aval_a_qty)) + # Check availability for product B + aval_b_qty = self.MoveObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(aval_b_qty, 5.0, 'Wrong move quantity availability of product B (%s found instead of 5)' % (aval_b_qty)) + # Check availability for product C + aval_c_qty = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(aval_c_qty, 3.0, 'Wrong move quantity availability of product C (%s found instead of 3)' % (aval_c_qty)) + # Check availability for product D + aval_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(aval_d_qty, 7.0, 'Wrong move quantity availability of product D (%s found instead of 7)' % (aval_d_qty)) + + # ---------------------------------------------------------------------- + # Replace pack operation of outgoing shipment. + # ---------------------------------------------------------------------- + + picking_out.do_prepare_partial() + self.StockPackObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', picking_out.id)]).write({'product_qty': 2.0}) + self.StockPackObj.search([('product_id', '=', self.productB.id), ('picking_id', '=', picking_out.id)]).write({'product_qty': 3.0}) + self.StockPackObj.create({ + 'product_id': self.productB.id, + 'product_qty': 2, + 'product_uom_id': self.productB.uom_id.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'picking_id': picking_out.id}) + self.StockPackObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', picking_out.id)]).write({ + 'product_qty': 2.0, 'lot_id': lot2_productC.id}) + self.StockPackObj.create({ + 'product_id': self.productC.id, + 'product_qty': 3, + 'product_uom_id': self.productC.uom_id.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'picking_id': picking_out.id}) + self.StockPackObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', picking_out.id)]).write({'product_qty': 6.0}) + + # Transfer picking. + picking_out.do_transfer() + + # ---------------------------------------------------------------------- + # Check state, quantity and total moves of outgoing shipment. + # ---------------------------------------------------------------------- + + # check outgoing shipment status. + self.assertEqual(picking_out.state, 'done', 'Wrong state of outgoing shipment.') + # check outgoing shipment total moves and and its state. + self.assertEqual(len(picking_out.move_lines), 5, 'Wrong number of move lines') + for move in picking_out.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + back_order_out = self.PickingObj.search([('backorder_id', '=', picking_out.id)]) + #------------------ + # Check back order. + # ----------------- + self.assertEqual(len(back_order_out), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(back_order_out.move_lines), 2, 'Wrong number of move lines') + # Check back order should be created with 8 quantity of product A. + product_a_qty = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', back_order_out.id)], limit=1).product_uom_qty + self.assertEqual(product_a_qty, 8.0, 'Wrong move quantity of product A (%s found instead of 8)' % (product_a_qty)) + # Check back order should be created with 4 quantity of product D. + product_d_qty = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_out.id)], limit=1).product_uom_qty + self.assertEqual(product_d_qty, 4.0, 'Wrong move quantity of product D (%s found instead of 4)' % (product_d_qty)) + + #----------------------------------------------------------------------- + # Check stock location quant quantity and quantity available + # of product A, B, C, D + #----------------------------------------------------------------------- + + # Check quants and available quantity for product A + quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + + self.assertEqual(sum(total_qty), 2.0, 'Expecting 2.0 Unit , got %.4f Unit on location stock!' % (sum(total_qty))) + self.assertEqual(self.productA.qty_available, 2.0, 'Wrong quantity available (%s found instead of 2.0)' % (self.productA.qty_available)) + # Check quants and available quantity for product B + quants = self.StockQuantObj.search([('product_id', '=', self.productB.id), ('location_id', '=', self.stock_location)]) + self.assertFalse(quants, 'No quant should found as outgoing shipment took everything out of stock.') + self.assertEqual(self.productB.qty_available, 0.0, 'Product B should have zero quantity available.') + # Check quants and available quantity for product C + quants = self.StockQuantObj.search([('product_id', '=', self.productC.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 2.0, 'Expecting 2.0 Unit, got %.4f Unit on location stock!' % (sum(total_qty))) + self.assertEqual(self.productC.qty_available, 2.0, 'Wrong quantity available (%s found instead of 2.0)' % (self.productC.qty_available)) + # Check quants and available quantity for product D + quant = self.StockQuantObj.search([('product_id', '=', self.productD.id), ('location_id', '=', self.stock_location)], limit=1) + self.assertEqual(quant.qty, 1.0, 'Expecting 1.0 Unit , got %.4f Unit on location stock!' % (quant.qty)) + self.assertEqual(self.productD.qty_available, 1.0, 'Wrong quantity available (%s found instead of 1.0)' % (self.productD.qty_available)) + #----------------------------------------------------------------------- + # Back Order of Incoming shipment + #----------------------------------------------------------------------- + + lot3_productC = LotObj.create({'name': 'Lot 3', 'product_id': self.productC.id}) + lot4_productC = LotObj.create({'name': 'Lot 4', 'product_id': self.productC.id}) + lot5_productC = LotObj.create({'name': 'Lot 5', 'product_id': self.productC.id}) + lot6_productC = LotObj.create({'name': 'Lot 6', 'product_id': self.productC.id}) + lot1_productD = LotObj.create({'name': 'Lot 1', 'product_id': self.productD.id}) + lot2_productD = LotObj.create({'name': 'Lot 2', 'product_id': self.productD.id}) + + # Confirm back order of incoming shipment. + back_order_in.action_confirm() + self.assertEqual(back_order_in.state, 'assigned', 'Wrong state of incoming shipment back order.') + for move in back_order_in.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + + # ---------------------------------------------------------------------- + # Replace pack operation (Back order of Incoming shipment) + # ---------------------------------------------------------------------- + + back_order_in.do_prepare_partial() + packD = self.StockPackObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_in.id)]) + self.assertEqual(len(packD), 1, 'Wrong number of pack operation.') + packD.write({'product_qty': 4, 'lot_id': lot1_productD.id}) + self.StockPackObj.create({ + 'product_id': self.productD.id, + 'product_qty': 4, + 'product_uom_id': self.productD.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': back_order_in.id, + 'lot_id': lot2_productD.id}) + self.StockPackObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', back_order_in.id)], limit=1).write({'product_qty': 1, 'lot_id': lot3_productC.id}) + self.StockPackObj.create({ + 'product_id': self.productC.id, + 'product_qty': 1, + 'product_uom_id': self.productC.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': back_order_in.id, + 'lot_id': lot4_productC.id}) + self.StockPackObj.create({ + 'product_id': self.productC.id, + 'product_qty': 2, + 'product_uom_id': self.productC.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': back_order_in.id, + 'lot_id': lot5_productC.id}) + self.StockPackObj.create({ + 'product_id': self.productC.id, + 'product_qty': 2, + 'product_uom_id': self.productC.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': back_order_in.id, + 'lot_id': lot6_productC.id}) + self.StockPackObj.create({ + 'product_id': self.productA.id, + 'product_qty': 10, + 'product_uom_id': self.productA.uom_id.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': back_order_in.id}) + back_order_in.do_transfer() + + # ---------------------------------------------------------------------- + # Check state, quantity and total moves (Back order of Incoming shipment). + # ---------------------------------------------------------------------- + + # Check total no of move lines. + self.assertEqual(len(back_order_in.move_lines), 6, 'Wrong number of move lines') + # Check incoming shipment state must be 'Done'. + self.assertEqual(back_order_in.state, 'done', 'Wrong state of picking.') + # Check incoming shipment move lines state must be 'Done'. + for move in back_order_in.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move lines.') + # Check product A done quantity must be 10 + movesA = self.MoveObj.search([('product_id', '=', self.productA.id), ('picking_id', '=', back_order_in.id)]) + self.assertEqual(movesA.product_uom_qty, 10, "Wrong move quantity of product A (%s found instead of 10)" % (movesA.product_uom_qty)) + # Check product C done quantity must be 3.0, 1.0, 2.0 + movesC = self.MoveObj.search([('product_id', '=', self.productC.id), ('picking_id', '=', back_order_in.id)]) + c_done_qty = [move.product_uom_qty for move in movesC] + self.assertEqual(set(c_done_qty), set([3.0, 1.0, 2.0]), 'Wrong quantity of moves product C.') + # Check product D done quantity must be 5.0 and 3.0 + movesD = self.MoveObj.search([('product_id', '=', self.productD.id), ('picking_id', '=', back_order_in.id)]) + d_done_qty = [move.product_uom_qty for move in movesD] + self.assertEqual(set(d_done_qty), set([3.0, 5.0]), 'Wrong quantity of moves product D.') + # Check no back order is created. + self.assertFalse(self.PickingObj.search([('backorder_id', '=', back_order_in.id)]), "Should not create any back order.") + + #----------------------------------------------------------------------- + # Check stock location quant quantity and quantity available + # of product A, B, C, D + #----------------------------------------------------------------------- + + # Check quants and available quantity for product A. + quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 12.0, 'Wrong total stock location quantity (%s found instead of 12)' % (sum(total_qty))) + self.assertEqual(self.productA.qty_available, 12.0, 'Wrong quantity available (%s found instead of 12)' % (self.productA.qty_available)) + # Check quants and available quantity for product B. + quants = self.StockQuantObj.search([('product_id', '=', self.productB.id), ('location_id', '=', self.stock_location)]) + self.assertFalse(quants, 'No quant should found as outgoing shipment took everything out of stock') + self.assertEqual(self.productB.qty_available, 0.0, 'Total quantity in stock should be 0 as the backorder took everything out of stock') + # Check quants and available quantity for product C. + quants = self.StockQuantObj.search([('product_id', '=', self.productC.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 8.0, 'Wrong total stock location quantity (%s found instead of 8)' % (sum(total_qty))) + self.assertEqual(self.productC.qty_available, 8.0, 'Wrong quantity available (%s found instead of 8)' % (self.productC.qty_available)) + # Check quants and available quantity for product D. + quants = self.StockQuantObj.search([('product_id', '=', self.productD.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 9.0, 'Wrong total stock location quantity (%s found instead of 9)' % (sum(total_qty))) + self.assertEqual(self.productD.qty_available, 9.0, 'Wrong quantity available (%s found instead of 9)' % (self.productD.qty_available)) + #----------------------------------------------------------------------- + # Back order of Outgoing shipment + # ---------------------------------------------------------------------- + + back_order_out.do_prepare_partial() + back_order_out.do_transfer() + + # Check stock location quants and available quantity for product A. + quants = self.StockQuantObj.search([('product_id', '=', self.productA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertGreaterEqual(float_round(sum(total_qty), precision_rounding=0.0001), 1, 'Total stock location quantity for product A should not be nagative.') + + def test_10_pickings_transfer_with_different_uom(self): + """ Picking transfer with diffrent unit of meassure. """ + + # ---------------------------------------------------------------------- + # Create incoming shipment of products DozA, SDozA, SDozARound, kgB, gB + # ---------------------------------------------------------------------- + # DozA ( 10 Dozen ) , SDozA ( 10.5 SuperDozen ) + # SDozARound ( 10.5 10.5 SuperDozenRound ) , kgB ( 0.020 kg ) + # gB ( 525.3 g ) + # ---------------------------------------------------------------------- + + picking_in_A = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in}) + self.MoveObj.create({ + 'name': self.DozA.name, + 'product_id': self.DozA.id, + 'product_uom_qty': 10, + 'product_uom': self.DozA.uom_id.id, + 'picking_id': picking_in_A.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.SDozA.name, + 'product_id': self.SDozA.id, + 'product_uom_qty': 10.5, + 'product_uom': self.SDozA.uom_id.id, + 'picking_id': picking_in_A.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.SDozARound.name, + 'product_id': self.SDozARound.id, + 'product_uom_qty': 10.5, + 'product_uom': self.SDozARound.uom_id.id, + 'picking_id': picking_in_A.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.kgB.name, + 'product_id': self.kgB.id, + 'product_uom_qty': 0.020, + 'product_uom': self.kgB.uom_id.id, + 'picking_id': picking_in_A.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.gB.name, + 'product_id': self.gB.id, + 'product_uom_qty': 525.3, + 'product_uom': self.gB.uom_id.id, + 'picking_id': picking_in_A.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + # Check incoming shipment move lines state. + for move in picking_in_A.move_lines: + self.assertEqual(move.state, 'draft', 'Move state must be draft.') + # Confirm incoming shipment. + picking_in_A.action_confirm() + # Check incoming shipment move lines state. + for move in picking_in_A.move_lines: + self.assertEqual(move.state, 'assigned', 'Move state must be draft.') + picking_in_A.do_prepare_partial() + + # ---------------------------------------------------- + # Check pack operation quantity of incoming shipments. + # ---------------------------------------------------- + PackSdozAround = self.StockPackObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_in_A.id)], limit=1) + self.assertEqual(PackSdozAround.product_qty, 11, 'Wrong quantity in pack operation (%s found instead of 11)' % (PackSdozAround.product_qty)) + picking_in_A.do_transfer() + #----------------------------------------------------------------------- + # Check stock location quant quantity and quantity available + #----------------------------------------------------------------------- + + # Check quants and available quantity for product DozA + quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 10, 'Expecting 10 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.DozA.qty_available, 10, 'Wrong quantity available (%s found instead of 10)' % (self.DozA.qty_available)) + # Check quants and available quantity for product SDozA + quants = self.StockQuantObj.search([('product_id', '=', self.SDozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 10.5, 'Expecting 10.5 SDozen , got %.4f SDozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.SDozA.qty_available, 10.5, 'Wrong quantity available (%s found instead of 10.5)' % (self.SDozA.qty_available)) + # Check quants and available quantity for product SDozARound + quants = self.StockQuantObj.search([('product_id', '=', self.SDozARound.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 11, 'Expecting 11 SDozenRound , got %.4f SDozenRound on location stock!' % (sum(total_qty))) + self.assertEqual(self.SDozARound.qty_available, 11, 'Wrong quantity available (%s found instead of 11)' % (self.SDozARound.qty_available)) + # Check quants and available quantity for product gB + quants = self.StockQuantObj.search([('product_id', '=', self.gB.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 525.3, 'Expecting 525.3 gram , got %.4f gram on location stock!' % (sum(total_qty))) + self.assertEqual(self.gB.qty_available, 525.3, 'Wrong quantity available (%s found instead of 525.3' % (self.gB.qty_available)) + # Check quants and available quantity for product kgB + quants = self.StockQuantObj.search([('product_id', '=', self.kgB.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 0.020, 'Expecting 0.020 kg , got %.4f kg on location stock!' % (sum(total_qty))) + self.assertEqual(self.kgB.qty_available, 0.020, 'Wrong quantity available (%s found instead of 0.020)' % (self.kgB.qty_available)) + + # ---------------------------------------------------------------------- + # Create Incoming Shipment B + # ---------------------------------------------------------------------- + + picking_in_B = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in}) + self.MoveObj.create({ + 'name': self.DozA.name, + 'product_id': self.DozA.id, + 'product_uom_qty': 120, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_in_B.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.SDozA.name, + 'product_id': self.SDozA.id, + 'product_uom_qty': 1512, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_in_B.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.SDozARound.name, + 'product_id': self.SDozARound.id, + 'product_uom_qty': 1584, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_in_B.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.kgB.name, + 'product_id': self.kgB.id, + 'product_uom_qty': 20.0, + 'product_uom': self.uom_gm.id, + 'picking_id': picking_in_B.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + self.MoveObj.create({ + 'name': self.gB.name, + 'product_id': self.gB.id, + 'product_uom_qty': 0.525, + 'product_uom': self.uom_kg.id, + 'picking_id': picking_in_B.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + # Check incoming shipment move lines state. + for move in picking_in_B.move_lines: + self.assertEqual(move.state, 'draft', 'Wrong state of move line.') + # Confirm incoming shipment. + picking_in_B.action_confirm() + # Check incoming shipment move lines state. + for move in picking_in_B.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + picking_in_B.do_prepare_partial() + + # ---------------------------------------------------------------------- + # Check product quantity and unit of measure of pack operaation. + # ---------------------------------------------------------------------- + + # Check pack operation quantity and unit of measure for product DozA. + PackdozA = self.StockPackObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(PackdozA.product_qty, 120, 'Wrong quantity in pack operation (%s found instead of 120)' % (PackdozA.product_qty)) + self.assertEqual(PackdozA.product_uom_id.id, self.uom_unit.id, 'Wrong uom in pack operation for product DozA.') + # Check pack operation quantity and unit of measure for product SDozA. + PackSdozA = self.StockPackObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(PackSdozA.product_qty, 1512, 'Wrong quantity in pack operation (%s found instead of 1512)' % (PackSdozA.product_qty)) + self.assertEqual(PackSdozA.product_uom_id.id, self.uom_unit.id, 'Wrong uom in pack operation for product SDozA.') + # Check pack operation quantity and unit of measure for product SDozARound. + PackSdozAround = self.StockPackObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(PackSdozAround.product_qty, 1584, 'Wrong quantity in pack operation (%s found instead of 1584)' % (PackSdozAround.product_qty)) + self.assertEqual(PackSdozAround.product_uom_id.id, self.uom_unit.id, 'Wrong uom in pack operation for product SDozARound.') + # Check pack operation quantity and unit of measure for product gB. + packgB = self.StockPackObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(packgB.product_qty, 525, 'Wrong quantity in pack operation (%s found instead of 525)' % (packgB.product_qty)) + self.assertEqual(packgB.product_uom_id.id, self.uom_gm.id, 'Wrong uom in pack operation for product gB.') + # Check pack operation quantity and unit of measure for product kgB. + packkgB = self.StockPackObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(packkgB.product_qty, 20.0, 'Wrong quantity in pack operation (%s found instead of 20)' % (packkgB.product_qty)) + self.assertEqual(packkgB.product_uom_id.id, self.uom_gm.id, 'Wrong uom in pack operation for product kgB') + + # ---------------------------------------------------------------------- + # Replace pack operation of incoming shipment. + # ---------------------------------------------------------------------- + + self.StockPackObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_in_B.id)]).write({ + 'product_qty': 0.020, 'product_uom_id': self.uom_kg.id}) + self.StockPackObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id)]).write({ + 'product_qty': 525.3, 'product_uom_id': self.uom_gm.id}) + self.StockPackObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_in_B.id)]).write({ + 'product_qty': 4, 'product_uom_id': self.uom_dozen.id}) + self.StockPackObj.create({ + 'product_id': self.DozA.id, + 'product_qty': 48, + 'product_uom_id': self.uom_unit.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_id': picking_in_B.id}) + + # Transfer product. + # ----------------- + picking_in_B.do_transfer() + + #----------------------------------------------------------------------- + # Check incoming shipment + #----------------------------------------------------------------------- + + # Check incoming shipment state. + self.assertEqual(picking_in_B.state, 'done', 'Incoming shipment state should be done.') + # Check incoming shipment move lines state. + for move in picking_in_B.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move line.') + # Check total done move lines for incoming shipment. + self.assertEqual(len(picking_in_B.move_lines), 6, 'Wrong number of move lines') + # Check product DozA done quantity. + moves_DozA = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(moves_DozA.product_uom_qty, 96, 'Wrong move quantity (%s found instead of 96)' % (moves_DozA.product_uom_qty)) + self.assertEqual(moves_DozA.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product DozA.') + # Check product SDozA done quantity. + moves_SDozA = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(moves_SDozA.product_uom_qty, 1512, 'Wrong move quantity (%s found instead of 1512)' % (moves_SDozA.product_uom_qty)) + self.assertEqual(moves_SDozA.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product SDozA.') + # Check product SDozARound done quantity. + moves_SDozARound = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(moves_SDozARound.product_uom_qty, 1584, 'Wrong move quantity (%s found instead of 1584)' % (moves_SDozARound.product_uom_qty)) + self.assertEqual(moves_SDozARound.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product SDozARound.') + # Check product kgB done quantity. + moves_kgB = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_in_B.id)], limit=1) + self.assertEqual(moves_kgB.product_uom_qty, 20, 'Wrong quantity in move (%s found instead of 20)' % (moves_kgB.product_uom_qty)) + self.assertEqual(moves_kgB.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product kgB.') + # Check two moves created for product gB with quantity (0.525 kg and 0.3 g) + moves_gB_kg = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id), ('product_uom', '=', self.uom_kg.id)], limit=1) + self.assertEqual(moves_gB_kg.product_uom_qty, 0.525, 'Wrong move quantity (%s found instead of 0.525)' % (moves_gB_kg.product_uom_qty)) + self.assertEqual(moves_gB_kg.product_uom.id, self.uom_kg.id, 'Wrong uom in move for product gB.') + moves_gB_g = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_in_B.id), ('product_uom', '=', self.uom_gm.id)], limit=1) + self.assertEqual(moves_gB_g.product_uom_qty, 0.3, 'Wrong move quantity (%s found instead of 0.3)' % (moves_gB_g.product_uom_qty)) + self.assertEqual(moves_gB_g.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product gB.') + + # ---------------------------------------------------------------------- + # Check Back order of Incoming shipment. + # ---------------------------------------------------------------------- + + # Check back order created or not. + bo_in_B = self.PickingObj.search([('backorder_id', '=', picking_in_B.id)]) + self.assertEqual(len(bo_in_B), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(bo_in_B.move_lines), 1, 'Wrong number of move lines') + # Check back order created with correct quantity and uom or not. + moves_DozA = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', bo_in_B.id)], limit=1) + self.assertEqual(moves_DozA.product_uom_qty, 24.0, 'Wrong move quantity (%s found instead of 0.525)' % (moves_DozA.product_uom_qty)) + self.assertEqual(moves_DozA.product_uom.id, self.uom_unit.id, 'Wrong uom in move for product DozA.') + + # ---------------------------------------------------------------------- + # Check product stock location quantity and quantity available. + # ---------------------------------------------------------------------- + + # Check quants and available quantity for product DozA + quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 18, 'Expecting 18 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.DozA.qty_available, 18, 'Wrong quantity available (%s found instead of 18)' % (self.DozA.qty_available)) + # Check quants and available quantity for product SDozA + quants = self.StockQuantObj.search([('product_id', '=', self.SDozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 21, 'Expecting 18 SDozen , got %.4f SDozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.SDozA.qty_available, 21, 'Wrong quantity available (%s found instead of 21)' % (self.SDozA.qty_available)) + # Check quants and available quantity for product SDozARound + quants = self.StockQuantObj.search([('product_id', '=', self.SDozARound.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 22, 'Expecting 22 SDozenRound , got %.4f SDozenRound on location stock!' % (sum(total_qty))) + self.assertEqual(self.SDozARound.qty_available, 22, 'Wrong quantity available (%s found instead of 22)' % (self.SDozARound.qty_available)) + # Check quants and available quantity for product gB. + quants = self.StockQuantObj.search([('product_id', '=', self.gB.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 1050.6, 'Expecting 1050.6 Gram , got %.4f Gram on location stock!' % (sum(total_qty))) + self.assertEqual(self.gB.qty_available, 1050.6, 'Wrong quantity available (%s found instead of 1050.6)' % (self.gB.qty_available)) + # Check quants and available quantity for product kgB. + quants = self.StockQuantObj.search([('product_id', '=', self.kgB.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 0.040, 'Expecting 0.040 kg , got %.4f kg on location stock!' % (sum(total_qty))) + self.assertEqual(self.kgB.qty_available, 0.040, 'Wrong quantity available (%s found instead of 0.040)' % (self.kgB.qty_available)) + + # ---------------------------------------------------------------------- + # Create outgoing shipment. + # ---------------------------------------------------------------------- + + before_out_quantity = self.kgB.qty_available + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out}) + self.MoveObj.create({ + 'name': self.kgB.name, + 'product_id': self.kgB.id, + 'product_uom_qty': 0.966, + 'product_uom': self.uom_gm.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.kgB.name, + 'product_id': self.kgB.id, + 'product_uom_qty': 0.034, + 'product_uom': self.uom_gm.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.action_confirm() + picking_out.action_assign() + picking_out.do_prepare_partial() + picking_out.do_transfer() + + # Check quantity difference after stock transfer. + quantity_diff = before_out_quantity - self.kgB.qty_available + self.assertEqual(float_round(quantity_diff, precision_rounding=0.0001), 0.001, 'Wrong quantity diffrence.') + self.assertEqual(self.kgB.qty_available, 0.039, 'Wrong quantity available (%s found instead of 0.039)' % (self.kgB.qty_available)) + + # ====================================================================== + # Outgoing shipments. + # ====================================================================== + # Create Outgoing shipment with ... + # product DozA ( 54 Unit ) , SDozA ( 288 Unit ) + # product SDozRound ( 360 unit ) , product gB ( 0.503 kg ) + # product kgB ( 19 g ) + # ====================================================================== + + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out}) + self.MoveObj.create({ + 'name': self.DozA.name, + 'product_id': self.DozA.id, + 'product_uom_qty': 54, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.SDozA.name, + 'product_id': self.SDozA.id, + 'product_uom_qty': 288, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.SDozARound.name, + 'product_id': self.SDozARound.id, + 'product_uom_qty': 360, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.gB.name, + 'product_id': self.gB.id, + 'product_uom_qty': 0.503, + 'product_uom': self.uom_kg.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + self.MoveObj.create({ + 'name': self.kgB.name, + 'product_id': self.kgB.id, + 'product_uom_qty': 20, + 'product_uom': self.uom_gm.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + # Confirm outgoing shipment. + picking_out.action_confirm() + for move in picking_out.move_lines: + self.assertEqual(move.state, 'confirmed', 'Wrong state of move line.') + # Assing product to outgoing shipments + picking_out.action_assign() + for move in picking_out.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + # Check product A available quantity + DozA_qty = self.MoveObj.search([('product_id', '=', self.DozA.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(DozA_qty, 4.5, 'Wrong move quantity availability (%s found instead of 4.5)' % (DozA_qty)) + # Check product B available quantity + SDozA_qty = self.MoveObj.search([('product_id', '=', self.SDozA.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(SDozA_qty, 2, 'Wrong move quantity availability (%s found instead of 2)' % (SDozA_qty)) + # Check product C available quantity + SDozARound_qty = self.MoveObj.search([('product_id', '=', self.SDozARound.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(SDozARound_qty, 3, 'Wrong move quantity availability (%s found instead of 3)' % (SDozARound_qty)) + # Check product D available quantity + gB_qty = self.MoveObj.search([('product_id', '=', self.gB.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(gB_qty, 503, 'Wrong move quantity availability (%s found instead of 503)' % (gB_qty)) + # Check product D available quantity + kgB_qty = self.MoveObj.search([('product_id', '=', self.kgB.id), ('picking_id', '=', picking_out.id)], limit=1).availability + self.assertEqual(kgB_qty, 0.020, 'Wrong move quantity availability (%s found instead of 0.020)' % (kgB_qty)) + + picking_out.action_confirm() + picking_out.action_assign() + picking_out.do_prepare_partial() + picking_out.do_transfer() + + # ---------------------------------------------------------------------- + # Check product stock location quantity and quantity available. + # ---------------------------------------------------------------------- + + # Check quants and available quantity for product DozA + quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 13.5, 'Expecting 13.5 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.DozA.qty_available, 13.5, 'Wrong quantity available (%s found instead of 13.5)' % (self.DozA.qty_available)) + # Check quants and available quantity for product SDozA + quants = self.StockQuantObj.search([('product_id', '=', self.SDozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 19, 'Expecting 19 SDozen , got %.4f SDozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.SDozA.qty_available, 19, 'Wrong quantity available (%s found instead of 19)' % (self.SDozA.qty_available)) + # Check quants and available quantity for product SDozARound + quants = self.StockQuantObj.search([('product_id', '=', self.SDozARound.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 19, 'Expecting 19 SDozRound , got %.4f SDozRound on location stock!' % (sum(total_qty))) + self.assertEqual(self.SDozARound.qty_available, 19, 'Wrong quantity available (%s found instead of 19)' % (self.SDozARound.qty_available)) + # Check quants and available quantity for product gB. + quants = self.StockQuantObj.search([('product_id', '=', self.gB.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(float_round(sum(total_qty), precision_rounding=0.0001), 547.6, 'Expecting 547.6 g , got %.4f g on location stock!' % (sum(total_qty))) + self.assertEqual(self.gB.qty_available, 547.6, 'Wrong quantity available (%s found instead of 547.6)' % (self.gB.qty_available)) + # Check quants and available quantity for product kgB. + quants = self.StockQuantObj.search([('product_id', '=', self.kgB.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 0.019, 'Expecting 0.019 kg , got %.4f kg on location stock!' % (sum(total_qty))) + self.assertEqual(self.kgB.qty_available, 0.019, 'Wrong quantity available (%s found instead of 0.019)' % (self.kgB.qty_available)) + # ---------------------------------------------------------------------- + # Receipt back order of incoming shipment. + # ---------------------------------------------------------------------- + + bo_in_B.do_prepare_partial() + bo_in_B.do_transfer() + # Check quants and available quantity for product kgB. + quants = self.StockQuantObj.search([('product_id', '=', self.DozA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 15.5, 'Expecting 15.5 Dozen , got %.4f Dozen on location stock!' % (sum(total_qty))) + self.assertEqual(self.DozA.qty_available, 15.5, 'Wrong quantity available (%s found instead of 15.5)' % (self.DozA.qty_available)) + + # ----------------------------------------- + # Create product in kg and receive in ton. + # ----------------------------------------- + + productKG = self.ProductObj.create({'name': 'Product KG', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id}) + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in}) + self.MoveObj.create({ + 'name': productKG.name, + 'product_id': productKG.id, + 'product_uom_qty': 1.0, + 'product_uom': self.uom_tone.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + # Check incoming shipment state. + self.assertEqual(picking_in.state, 'draft', 'Incoming shipment state should be draft.') + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'draft', 'Wrong state of move line.') + # Confirm incoming shipment. + picking_in.action_confirm() + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'assigned', 'Wrong state of move line.') + picking_in.do_prepare_partial() + # Check pack operation quantity. + packKG = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', picking_in.id)], limit=1) + self.assertEqual(packKG.product_qty, 1000, 'Wrong product quantity in pack operation (%s found instead of 1000)' % (packKG.product_qty)) + self.assertEqual(packKG.product_uom_id.id, self.uom_kg.id, 'Wrong product uom in pack operation.') + # Transfer Incoming shipment. + picking_in.do_transfer() + + #----------------------------------------------------------------------- + # Check incoming shipment after transfer. + #----------------------------------------------------------------------- + + # Check incoming shipment state. + self.assertEqual(picking_in.state, 'done', 'Incoming shipment state should be done.') + # Check incoming shipment move lines state. + for move in picking_in.move_lines: + self.assertEqual(move.state, 'done', 'Wrong state of move lines.') + # Check total done move lines for incoming shipment. + self.assertEqual(len(picking_in.move_lines), 1, 'Wrong number of move lines') + # Check product DozA done quantity. + move = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', picking_in.id)], limit=1) + self.assertEqual(move.product_uom_qty, 1, 'Wrong product quantity in done move.') + self.assertEqual(move.product_uom.id, self.uom_tone.id, 'Wrong unit of measure in done move.') + self.assertEqual(productKG.qty_available, 1000, 'Wrong quantity available of product (%s found instead of 1000)' % (productKG.qty_available)) + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out}) + self.MoveObj.create({ + 'name': productKG.name, + 'product_id': productKG.id, + 'product_uom_qty': 2.5, + 'product_uom': self.uom_gm.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.action_confirm() + picking_out.action_assign() + picking_out.do_prepare_partial() + pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', picking_out.id)], limit=1) + pack_opt.write({'product_qty': 0.5}) + picking_out.do_transfer() + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + # Check total quantity stock location. + self.assertEqual(sum(total_qty), 999.9995, 'Expecting 999.9995 kg , got %.4f kg on location stock!' % (sum(total_qty))) + + # Check Back order created or not. + #--------------------------------- + bo_out_1 = self.PickingObj.search([('backorder_id', '=', picking_out.id)]) + self.assertEqual(len(bo_out_1), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(bo_out_1.move_lines), 1, 'Wrong number of move lines') + moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_1.id)], limit=1) + # Check back order created with correct quantity and uom or not. + self.assertEqual(moves_KG.product_uom_qty, 2.0, 'Wrong move quantity (%s found instead of 2.0)' % (moves_KG.product_uom_qty)) + self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.') + bo_out_1.action_assign() + bo_out_1.do_prepare_partial() + pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_1.id)], limit=1) + pack_opt.write({'product_qty': 0.5}) + bo_out_1.do_transfer() + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + + # Check total quantity stock location. + self.assertEqual(sum(total_qty), 999.9990, 'Expecting 999.9990 kg , got %.4f kg on location stock!' % (sum(total_qty))) + + # Check Back order created or not. + #--------------------------------- + bo_out_2 = self.PickingObj.search([('backorder_id', '=', bo_out_1.id)]) + self.assertEqual(len(bo_out_2), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(bo_out_2.move_lines), 1, 'Wrong number of move lines') + # Check back order created with correct move quantity and uom or not. + moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_2.id)], limit=1) + self.assertEqual(moves_KG.product_uom_qty, 1.5, 'Wrong move quantity (%s found instead of 1.5)' % (moves_KG.product_uom_qty)) + self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.') + bo_out_2.action_assign() + bo_out_2.do_prepare_partial() + pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_2.id)], limit=1) + pack_opt.write({'product_qty': 0.5}) + bo_out_2.do_transfer() + # Check total quantity stock location of product KG. + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 999.9985, 'Expecting 999.9985 kg , got %.4f kg on location stock!' % (sum(total_qty))) + + # Check Back order created or not. + #--------------------------------- + bo_out_3 = self.PickingObj.search([('backorder_id', '=', bo_out_2.id)]) + self.assertEqual(len(bo_out_3), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(bo_out_3.move_lines), 1, 'Wrong number of move lines') + # Check back order created with correct quantity and uom or not. + moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_3.id)], limit=1) + self.assertEqual(moves_KG.product_uom_qty, 1, 'Wrong move quantity (%s found instead of 1.0)' % (moves_KG.product_uom_qty)) + self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.') + bo_out_3.action_assign() + bo_out_3.do_prepare_partial() + pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_3.id)], limit=1) + pack_opt.write({'product_qty': 0.5}) + bo_out_3.do_transfer() + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 999.9980, 'Expecting 999.9980 kg , got %.4f kg on location stock!' % (sum(total_qty))) + + # Check Back order created or not. + #--------------------------------- + bo_out_4 = self.PickingObj.search([('backorder_id', '=', bo_out_3.id)]) + + self.assertEqual(len(bo_out_4), 1, 'Back order should be created.') + # Check total move lines of back order. + self.assertEqual(len(bo_out_4.move_lines), 1, 'Wrong number of move lines') + # Check back order created with correct quantity and uom or not. + moves_KG = self.MoveObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_4.id)], limit=1) + self.assertEqual(moves_KG.product_uom_qty, 0.5, 'Wrong move quantity (%s found instead of 0.5)' % (moves_KG.product_uom_qty)) + self.assertEqual(moves_KG.product_uom.id, self.uom_gm.id, 'Wrong uom in move for product KG.') + bo_out_4.action_assign() + bo_out_4.do_prepare_partial() + pack_opt = self.StockPackObj.search([('product_id', '=', productKG.id), ('picking_id', '=', bo_out_4.id)], limit=1) + pack_opt.write({'product_qty': 0.5}) + bo_out_4.do_transfer() + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 999.9975, 'Expecting 999.9975 kg , got %.4f kg on location stock!' % (sum(total_qty))) + + def test_20_create_inventory_with_different_uom(self): + """Create inventory with different unit of measure.""" + + # ------------------------------------------------ + # Test inventory with product A(Unit). + # ------------------------------------------------ + + inventory = self.InvObj.create({'name': 'Test', + 'product_id': self.UnitA.id, + 'filter': 'product'}) + inventory.prepare_inventory() + self.assertFalse(inventory.line_ids, "Inventory line should not created.") + inventory_line = self.InvLineObj.create({ + 'inventory_id': inventory.id, + 'product_id': self.UnitA.id, + 'product_uom_id': self.uom_dozen.id, + 'product_qty': 10, + 'location_id': self.stock_location}) + inventory.action_done() + # Check quantity available of product UnitA. + quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 120, 'Expecting 120 Units , got %.4f Units on location stock!' % (sum(total_qty))) + self.assertEqual(self.UnitA.qty_available, 120, 'Expecting 120 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available)) + # Create Inventory again for product UnitA. + inventory = self.InvObj.create({'name': 'Test', + 'product_id': self.UnitA.id, + 'filter': 'product'}) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.") + inventory_line = self.InvLineObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1) + self.assertEqual(inventory_line.product_qty, 120, "Wrong product quantity in inventory line.") + # Modify the inventory line and set the quantity to 144 product on this new inventory. + inventory_line.write({'product_qty': 144}) + inventory.action_done() + move = self.MoveObj.search([('product_id', '=', self.UnitA.id), ('inventory_id', '=', inventory.id)], limit=1) + self.assertEqual(move.product_uom_qty, 24, "Wrong move quantity of product UnitA.") + # Check quantity available of product UnitA. + quants = self.StockQuantObj.search([('product_id', '=', self.UnitA.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 144, 'Expecting 144 Units , got %.4f Units on location stock!' % (sum(total_qty))) + self.assertEqual(self.UnitA.qty_available, 144, 'Expecting 144 Units , got %.4f Units of quantity available!' % (self.UnitA.qty_available)) + + # ------------------------------------------------ + # Test inventory with product KG. + # ------------------------------------------------ + + productKG = self.ProductObj.create({'name': 'Product KG', 'uom_id': self.uom_kg.id, 'uom_po_id': self.uom_kg.id}) + inventory = self.InvObj.create({'name': 'Inventory Product KG', + 'product_id': productKG.id, + 'filter': 'product'}) + inventory.prepare_inventory() + self.assertFalse(inventory.line_ids, "Inventory line should not created.") + inventory_line = self.InvLineObj.create({ + 'inventory_id': inventory.id, + 'product_id': productKG.id, + 'product_uom_id': self.uom_tone.id, + 'product_qty': 5, + 'location_id': self.stock_location}) + inventory.action_done() + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 5000, 'Expecting 5000 kg , got %.4f kg on location stock!' % (sum(total_qty))) + self.assertEqual(productKG.qty_available, 5000, 'Expecting 5000 kg , got %.4f kg of quantity available!' % (productKG.qty_available)) + # Create Inventory again. + inventory = self.InvObj.create({'name': 'Test', + 'product_id': productKG.id, + 'filter': 'product'}) + inventory.prepare_inventory() + self.assertEqual(len(inventory.line_ids), 1, "One inventory line should be created.") + inventory_line = self.InvLineObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) + self.assertEqual(inventory_line.product_qty, 5000, "Wrong product quantity in inventory line.") + # Modify the inventory line and set the quantity to 4000 product on this new inventory. + inventory_line.write({'product_qty': 4000}) + inventory.action_done() + # Check inventory move quantity of product KG. + move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) + self.assertEqual(move.product_uom_qty, 1000, "Wrong move quantity of product KG.") + # Check quantity available of product KG. + quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)]) + total_qty = [quant.qty for quant in quants] + self.assertEqual(sum(total_qty), 4000, 'Expecting 4000 kg , got %.4f on location stock!' % (sum(total_qty))) + self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available)) diff --git a/addons/stock/views/report_stockpicking.xml b/addons/stock/views/report_stockpicking.xml index 97131958ded..ba41f2b2210 100644 --- a/addons/stock/views/report_stockpicking.xml +++ b/addons/stock/views/report_stockpicking.xml @@ -9,26 +9,41 @@ <div class="row"><div class="col-xs-4 pull-right"> <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', o.name, 600, 100)" style="width:300px;height:50px;"/> </div></div> - <div t-if="o.picking_type_id.code=='incoming' and o.partner_id"> - <span><strong>Supplier Address:</strong></span> - </div> - <div t-if="o.picking_type_id.code=='internal' and o.partner_id"> - <span><strong>Warehouse Address:</strong></span> - </div> - <div t-if="o.picking_type_id.code=='outgoing' and o.partner_id"> - <span><strong>Customer Address:</strong></span> - </div> - <div t-if="o.partner_id" name="partner_header"> - <div t-field="o.partner_id" - t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> - <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p> - </div> - - <h2> - <span t-field="o.picking_type_id"/>: - <span t-field="o.name"/> - </h2> - <table class="table table-condensed"> + <div class="row"> + <div class="col-xs-6"> + <div t-if="o.picking_type_id.code=='incoming' and o.partner_id"> + <span><strong>Supplier Address:</strong></span> + </div> + <div t-if="o.picking_type_id.code=='internal' and o.partner_id"> + <span><strong>Warehouse Address:</strong></span> + </div> + <div t-if="o.picking_type_id.code=='outgoing' and o.partner_id"> + <span><strong>Customer Address:</strong></span> + </div> + <div t-if="o.partner_id" name="partner_header"> + <div t-field="o.partner_id" + t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> + <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p> + </div> + </div> + <div class="col-xs-5 col-xs-offset-1"> + <div t-if="o.move_lines[0].partner_id and o.move_lines[0].partner_id.id != o.partner_id.id"> + <span><strong>Delivery Address:</strong></span> + <div t-field="o.move_lines[0].partner_id" + t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> + </div> + <div t-if="o.picking_type_id.code != 'internal' and (not o.move_lines[0].partner_id) and o.picking_type_id.warehouse_id.partner_id"> + <span><strong>Warehouse Address:</strong></span> + <div t-field="o.picking_type_id.warehouse_id.partner_id" + t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> + </div> + </div> + </div> + <h2> + <span t-field="o.picking_type_id"/>: + <span t-field="o.name"/> + </h2> + <table class="table table-condensed"> <thead> <tr> <th><strong>Order (Origin)</strong></th> diff --git a/addons/stock_account/__openerp__.py b/addons/stock_account/__openerp__.py index 9774b75427b..5db699845da 100644 --- a/addons/stock_account/__openerp__.py +++ b/addons/stock_account/__openerp__.py @@ -39,7 +39,6 @@ Dashboard / Reports for Warehouse Management includes: * Stock Inventory Value at given date (support dates in the past) """, 'website': 'https://www.odoo.com/page/warehouse', - 'images': [], 'depends': ['stock', 'account'], 'category': 'Hidden', 'sequence': 16, diff --git a/addons/stock_account/stock.py b/addons/stock_account/stock.py index 587f135d259..b965f6689d7 100644 --- a/addons/stock_account/stock.py +++ b/addons/stock_account/stock.py @@ -292,7 +292,9 @@ class stock_picking(osv.osv): invoices[key] = invoice_id else: invoice = invoice_obj.browse(cr, uid, invoices[key], context=context) - invoice.write({'origin': '%s, %s' % (invoice.origin, invoice_vals['origin'],)}) + if not invoice.origin or invoice_vals['origin'] not in invoice.origin.split(', '): + origin = filter(None, [invoice.origin, invoice_vals['origin']]) + invoice.write({'origin': ', '.join(origin)}) invoice_line_vals = move_obj._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context) invoice_line_vals['invoice_id'] = invoices[key] diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py index 06ef0aeefc9..193b19cbc7b 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/wizard/stock_valuation_history.py @@ -49,20 +49,19 @@ class stock_history(osv.osv): prod_dict = {} if 'inventory_value' in fields: for line in res: - if '__domain' in line: - lines = self.search(cr, uid, line['__domain'], context=context) - inv_value = 0.0 - product_tmpl_obj = self.pool.get("product.template") - lines_rec = self.browse(cr, uid, lines, context=context) - for line_rec in lines_rec: - if line_rec.product_id.cost_method == 'real': - price = line_rec.price_unit_on_quant - else: - if not line_rec.product_id.id in prod_dict: - prod_dict[line_rec.product_id.id] = product_tmpl_obj.get_history_price(cr, uid, line_rec.product_id.product_tmpl_id.id, line_rec.company_id.id, date=date, context=context) - price = prod_dict[line_rec.product_id.id] - inv_value += price * line_rec.quantity - line['inventory_value'] = inv_value + lines = self.search(cr, uid, line.get('__domain', []), context=context) + inv_value = 0.0 + product_tmpl_obj = self.pool.get("product.template") + lines_rec = self.browse(cr, uid, lines, context=context) + for line_rec in lines_rec: + if line_rec.product_id.cost_method == 'real': + price = line_rec.price_unit_on_quant + else: + if not line_rec.product_id.id in prod_dict: + prod_dict[line_rec.product_id.id] = product_tmpl_obj.get_history_price(cr, uid, line_rec.product_id.product_tmpl_id.id, line_rec.company_id.id, date=date, context=context) + price = prod_dict[line_rec.product_id.id] + inv_value += price * line_rec.quantity + line['inventory_value'] = inv_value return res def _get_inventory_value(self, cr, uid, ids, name, attr, context=None): diff --git a/addons/stock_account/wizard/stock_valuation_history_view.xml b/addons/stock_account/wizard/stock_valuation_history_view.xml index 5ca5fa4dc5d..d2026b5a5ab 100644 --- a/addons/stock_account/wizard/stock_valuation_history_view.xml +++ b/addons/stock_account/wizard/stock_valuation_history_view.xml @@ -82,7 +82,7 @@ <field name="arch" type="xml"> <search string="Stock Value At Date"> <field name="product_id"/> - <field name="location_id" filter_domain="[('name', 'child_of', self)]"/> + <field name="location_id" filter_domain="[('location_id', 'child_of', self)]"/> <field name="company_id" groups="base.group_multi_company"/> <group expand="1" string="Group By"> <filter string="Product" icon="terp-accessories-archiver" context="{'group_by':'product_id'}" name='group_by_product'/> diff --git a/addons/stock_dropshipping/__openerp__.py b/addons/stock_dropshipping/__openerp__.py index 6f58d65efe9..2ff27230c04 100644 --- a/addons/stock_dropshipping/__openerp__.py +++ b/addons/stock_dropshipping/__openerp__.py @@ -40,7 +40,6 @@ internal transfer document is needed. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/warehouse', - 'images': [], 'depends': ['purchase', 'sale_stock'], 'data': ['stock_dropshipping.xml'], 'test': [ diff --git a/addons/stock_invoice_directly/__openerp__.py b/addons/stock_invoice_directly/__openerp__.py index ed046460f4f..bb107c3e3ee 100644 --- a/addons/stock_invoice_directly/__openerp__.py +++ b/addons/stock_invoice_directly/__openerp__.py @@ -33,7 +33,6 @@ wizard if the delivery is to be invoiced. """, 'author': 'OpenERP SA', 'website': 'https://www.odoo.com/page/warehouse', - 'images': ['images/create_invoice.jpeg'], 'depends': ['delivery', 'stock'], 'data': [], 'demo': [], diff --git a/addons/stock_landed_costs/__openerp__.py b/addons/stock_landed_costs/__openerp__.py index bb673d4fc3e..29012bdd8a3 100644 --- a/addons/stock_landed_costs/__openerp__.py +++ b/addons/stock_landed_costs/__openerp__.py @@ -30,7 +30,6 @@ Landed Costs Management This module allows you to easily add extra costs on pickings and decide the split of these costs among their stock moves in order to take them into account in your stock valuation. """, 'website': 'https://www.odoo.com/page/warehouse', - 'images': [], 'depends': ['stock_account'], 'category': 'Warehouse Management', 'sequence': 16, diff --git a/addons/subscription/__openerp__.py b/addons/subscription/__openerp__.py index 4c2620b4d85..68e059016d2 100644 --- a/addons/subscription/__openerp__.py +++ b/addons/subscription/__openerp__.py @@ -42,5 +42,4 @@ e.g. To have an invoice generated automatically periodically: 'demo': ['subscription_demo.xml',], 'installable': True, 'auto_install': False, - 'images': ['images/subscription_document_fields.jpeg','images/subscriptions.jpeg'], } diff --git a/addons/survey/__openerp__.py b/addons/survey/__openerp__.py index dcac9ab75fc..7477a76c3e5 100644 --- a/addons/survey/__openerp__.py +++ b/addons/survey/__openerp__.py @@ -55,5 +55,4 @@ sent mails with personal token for the invitation of the survey. 'auto_install': False, 'application': True, 'sequence': 10, - 'images': [], } diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 151df516f3b..511d70896c3 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -30,7 +30,6 @@ from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF, ustr -from openerp.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -167,6 +166,11 @@ class WebsiteSurvey(http.Controller): elif user_input.state == 'skip': flag = (True if prev and prev == 'prev' else False) page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context) + + #special case if you click "previous" from the last page, then leave the survey, then reopen it from the URL, avoid crash + if not page: + page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=True, context=context) + data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token} if last: data.update({'last': True}) diff --git a/addons/survey/static/src/js/survey.js b/addons/survey/static/src/js/survey.js index d8c1d07f300..778bc24ea3d 100644 --- a/addons/survey/static/src/js/survey.js +++ b/addons/survey/static/src/js/survey.js @@ -39,7 +39,7 @@ openerp.website.if_dom_contains('.js_surveyform', function (the_form) { print_mode = true; } - // Quizz correction mode + // Quiz correction mode if (! _.isUndefined(scores_controller)) { quiz_correction_mode = true; } @@ -79,33 +79,48 @@ openerp.website.if_dom_contains('.js_surveyform', function (the_form) { // Pre-filling of the form with previous answers function prefill(){ - var prefill_def = $.ajax(prefill_controller, {dataType: "json"}) - .done(function(json_data){ - _.each(json_data, function(value, key){ - the_form.find(".form-control[name=" + key + "]").val(value); - the_form.find("input[name^=" + key + "]").each(function(){ - $(this).val(value); + if (! _.isUndefined(prefill_controller)) { + var prefill_def = $.ajax(prefill_controller, {dataType: "json"}) + .done(function(json_data){ + _.each(json_data, function(value, key){ + + // prefill of text/number/date boxes + var input = the_form.find(".form-control[name=" + key + "]"); + input.val(value); + + // special case for comments under multiple suggestions questions + if (_.string.endsWith(key, "_comment") && + (input.parent().hasClass("js_comments") || input.parent().hasClass("js_ck_comments"))) { + input.siblings().find('>input').attr("checked","checked"); + } + + // checkboxes and radios + the_form.find("input[name^=" + key + "][type!='text']").each(function(){ + $(this).val(value); + }); }); + }) + .fail(function(){ + console.warn("[survey] Unable to load prefill data"); }); - }) - .fail(function(){ - console.warn("[survey] Unable to load prefill data"); - }); - return prefill_def; + return prefill_def; + } } // Display score if quiz correction mode function display_scores(){ - var score_def = $.ajax(scores_controller, {dataType: "json"}) - .done(function(json_data){ - _.each(json_data, function(value, key){ - the_form.find("span[data-score-question=" + key + "]").text("Your score: " + value); + if (! _.isUndefined(scores_controller)) { + var score_def = $.ajax(scores_controller, {dataType: "json"}) + .done(function(json_data){ + _.each(json_data, function(value, key){ + the_form.find("span[data-score-question=" + key + "]").text("Your score: " + value); + }); + }) + .fail(function(){ + console.warn("[survey] Unable to load score data"); }); - }) - .fail(function(){ - console.warn("[survey] Unable to load score data"); - }); - return score_def; + return score_def; + } } // Parameters for form submission diff --git a/addons/survey/survey.py b/addons/survey/survey.py index da84d8ddc8c..612ce78abfa 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -201,7 +201,7 @@ class survey_survey(osv.Model): 'Email Template', ondelete='set null'), 'thank_you_message': fields.html('Thank you message', translate=True, help="This message will be displayed when survey is completed"), - 'quizz_mode': fields.boolean(string='Quizz mode') + 'quizz_mode': fields.boolean(string='Quiz mode') } def _default_stage(self, cr, uid, context=None): diff --git a/addons/survey/test/__init__.py b/addons/survey/test/__init__.py index b2a1a5accf2..ee6793c4b7c 100644 --- a/addons/survey/test/__init__.py +++ b/addons/survey/test/__init__.py @@ -21,7 +21,3 @@ from . import test_survey - -checks = [ - test_survey -] diff --git a/addons/warning/__openerp__.py b/addons/warning/__openerp__.py index a991f42fce0..e7e72738819 100644 --- a/addons/warning/__openerp__.py +++ b/addons/warning/__openerp__.py @@ -37,5 +37,4 @@ picking and invoice. The message is triggered by the form's onchange event. 'demo': [], 'installable': True, 'auto_install': False, - 'images': ['images/customers_warnings.jpeg','images/sale_order_warning.jpeg'], } diff --git a/addons/warning/warning.py b/addons/warning/warning.py index 81bac3cdff5..954b0e96e95 100644 --- a/addons/warning/warning.py +++ b/addons/warning/warning.py @@ -76,7 +76,9 @@ class sale_order(osv.osv): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] warning['message'] = message and message + ' ' + result['warning']['message'] or result['warning']['message'] - return {'value': result.get('value',{}), 'warning':warning} + if warning: + result['warning'] = warning + return result class purchase_order(osv.osv): @@ -104,7 +106,9 @@ class purchase_order(osv.osv): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] warning['message'] = message and message + ' ' + result['warning']['message'] or result['warning']['message'] - return {'value': result.get('value',{}), 'warning':warning} + if warning: + result['warning'] = warning + return result @@ -143,7 +147,9 @@ class account_invoice(osv.osv): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] warning['message'] = message and message + ' ' + result['warning']['message'] or result['warning']['message'] - return {'value': result.get('value',{}), 'warning':warning} + if warning: + result['warning'] = warning + return result class stock_picking(osv.osv): @@ -171,8 +177,9 @@ class stock_picking(osv.osv): warning['title'] = title and title +' & '+ result['warning']['title'] or result['warning']['title'] warning['message'] = message and message + ' ' + result['warning']['message'] or result['warning']['message'] - return {'value': result.get('value',{}), 'warning':warning} - + if warning: + result['warning'] = warning + return result class product_product(osv.osv): _inherit = 'product.template' @@ -221,7 +228,9 @@ class sale_order_line(osv.osv): warning['title'] = title and title +' & '+result['warning']['title'] or result['warning']['title'] warning['message'] = message and message +'\n\n'+result['warning']['message'] or result['warning']['message'] - return {'value': result.get('value',{}), 'warning':warning} + if warning: + result['warning'] = warning + return result class purchase_order_line(osv.osv): @@ -252,4 +261,6 @@ class purchase_order_line(osv.osv): warning['title'] = title and title +' & '+result['warning']['title'] or result['warning']['title'] warning['message'] = message and message +'\n\n'+result['warning']['message'] or result['warning']['message'] - return {'value': result.get('value',{}), 'warning':warning} + if warning: + result['warning'] = warning + return result diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index e95e8f3c58b..cec9ae94837 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -744,24 +744,21 @@ class Database(http.Controller): return {'error': _('Could not drop database !'), 'title': _('Drop Database')} @http.route('/web/database/backup', type='http', auth="none") - def backup(self, backup_db, backup_pwd, token, **kwargs): + def backup(self, backup_db, backup_pwd, token, backup_format='zip'): try: - format = kwargs.get('format') - ext = "zip" if format == 'zip' else "dump" - db_dump = base64.b64decode( - request.session.proxy("db").dump(backup_pwd, backup_db, format)) - filename = "%(db)s_%(timestamp)s.%(ext)s" % { - 'db': backup_db, - 'timestamp': datetime.datetime.utcnow().strftime( - "%Y-%m-%d_%H-%M-%SZ"), - 'ext': ext - } - return request.make_response(db_dump, - [('Content-Type', 'application/octet-stream; charset=binary'), - ('Content-Disposition', content_disposition(filename))], - {'fileToken': token} - ) + openerp.service.security.check_super(backup_pwd) + ts = datetime.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") + filename = "%s_%s.%s" % (backup_db, ts, backup_format) + headers = [ + ('Content-Type', 'application/octet-stream; charset=binary'), + ('Content-Disposition', content_disposition(filename)), + ] + dump_stream = openerp.service.db.dump_db(backup_db, None, backup_format) + response = werkzeug.wrappers.Response(dump_stream, headers=headers, direct_passthrough=True) + response.set_cookie('fileToken', token) + return response except Exception, e: + _logger.exception('Database.backup') return simplejson.dumps([[],[{'error': openerp.tools.ustr(e), 'title': _('Backup Database')}]]) @http.route('/web/database/restore', type='http', auth="none") diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 296c78d8d6d..abbc1330804 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3554,7 +3554,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc reinit_value: function(val) { this.internal_set_value(val); this.floating = false; - if (this.is_started) + if (this.is_started && !this.no_rerender) this.render_value(); }, initialize_field: function() { @@ -3805,7 +3805,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc } if (! no_recurse) { var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); - this.alive(dataset.name_get([self.get("value")])).done(function(data) { + var def = this.alive(dataset.name_get([self.get("value")])).done(function(data) { if (!data[0]) { self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value"))); return; @@ -3818,6 +3818,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")]; self.render_value(true); }); + if (this.view && this.view.render_value_defs){ + this.view.render_value_defs.push(def); + } } }, display_string: function(str) { @@ -4293,7 +4296,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.dataset.index = 0; } this.trigger_on_change(); - if (this.is_started) { + if (this.is_started && !this.no_rerender) { return self.reload_current_view(); } else { return $.when(); @@ -4437,12 +4440,19 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ return true; } var r; - return _.every(this.records.records, function(record){ + if (_.isEmpty(this.records.records)){ + return true; + } + current_values = {}; + _.each(this.editor.form.fields, function(field){ + field._inhibit_on_change_flag = true; + field.no_rerender = true; + current_values[field.name] = field.get('value'); + }); + var valid = _.every(this.records.records, function(record){ r = record; _.each(self.editor.form.fields, function(field){ - field._inhibit_on_change_flag = true; field.set_value(r.attributes[field.name]); - field._inhibit_on_change_flag = false; }); return _.every(self.editor.form.fields, function(field){ field.process_modifiers(); @@ -4450,6 +4460,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ return field.is_valid(); }); }); + _.each(this.editor.form.fields, function(field){ + field.set('value', current_values[field.name]); + field._inhibit_on_change_flag = false; + field.no_rerender = false; + }); + return valid; }, do_add_record: function () { if (this.editable()) { diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index 831d383c731..77559504060 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -231,8 +231,9 @@ this.records.add(record, { at: this.prepends_on_create() ? 0 : null}); } - - return this.ensure_saved().then(function () { + return this.ensure_saved().then(function(){ + return $.when.apply(null, self.editor.form.render_value_defs); + }).then(function () { var $recordRow = self.groups.get_row_for(record); var cells = self.get_cells_for($recordRow); var fields = {}; diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 68915077a48..06e19e28ca9 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -222,27 +222,25 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie var local_context = { active_model: self.dataset.model, active_id: id, - active_ids: [id]}; + active_ids: [id] + }; + var ctx = instance.web.pyeval.eval( + 'context', new instance.web.CompoundContext( + this.dataset.get_context(), local_context)); return this.rpc('/web/treeview/action', { id: id, model: this.dataset.model, - context: instance.web.pyeval.eval( - 'context', new instance.web.CompoundContext( - this.dataset.get_context(), local_context)) + context: ctx }).then(function (actions) { if (!actions.length) { return; } var action = actions[0][2]; - var c = new instance.web.CompoundContext(local_context); + var c = new instance.web.CompoundContext(local_context).set_eval_context(ctx); if (action.context) { c.add(action.context); } - return instance.web.pyeval.eval_domains_and_contexts({ - contexts: [c], domains: [] - }).then(function (res) { - action.context = res.context; - return self.do_action(action); - }, null); - }, null); + action.context = c; + return self.do_action(action); + }); }, // show & hide the contents diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index b0bceafc5a3..7d053f81700 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -216,6 +216,15 @@ </td> </tr> <tr> + <td><label for="backup_format">Backup format:</label></td> + <td class="oe_form_field oe_form_field_selection"> + <select name="backup_format"> + <option value="zip">zip (includes filestore)</option> + <option value="dump">pg_dump custom format (without filestore)</option> + </select> + </td> + </tr> + <tr> <td><label for="backup_pwd">Master Password:</label></td> <td><input type="password" name="backup_pwd" class="required" /></td> </tr> diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html index da2c1a9f41a..79a193f283f 100644 --- a/addons/web/views/database_manager.html +++ b/addons/web/views/database_manager.html @@ -3,7 +3,7 @@ <head> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> - <title>OpenERP</title> + <title>Odoo</title> <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/> <link rel="stylesheet" href="/web/static/src/css/full.css" /> <script src="/web/static/lib/es5-shim/es5-shim.min.js" type="text/javascript"></script> @@ -104,4 +104,4 @@ <script>CFInstall.check({mode: "overlay"});</script> <![endif]--> </body> -</html>
\ No newline at end of file +</html> diff --git a/addons/web/views/database_selector.html b/addons/web/views/database_selector.html index 8164ddb9498..67c82c6c260 100644 --- a/addons/web/views/database_selector.html +++ b/addons/web/views/database_selector.html @@ -2,7 +2,7 @@ <html style="height: 100%"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"> - <title>OpenERP</title> + <title>Odoo</title> <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"> <link rel="stylesheet" href="/web/static/src/css/full.css"> <link rel="stylesheet" href="/web/static/lib/fontawesome/css/font-awesome.css"> diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 513a15676d2..e49773028c9 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -78,7 +78,8 @@ def is_multilang_url(local_url, langs=None): path = url[0] query_string = url[1] if len(url) > 1 else None router = request.httprequest.app.get_db_router(request.db).bind('') - func = router.match(path, query_args=query_string)[0] + # Force to check method to POST. Odoo uses methods : ['POST'] and ['GET', 'POST'] + func = router.match(path, method='POST', query_args=query_string)[0] return func.routing.get('website', False) and func.routing.get('multilang', True) except Exception: return False diff --git a/addons/website/static/src/css/snippets.css b/addons/website/static/src/css/snippets.css index 83e5a83a38c..de7cef637bb 100644 --- a/addons/website/static/src/css/snippets.css +++ b/addons/website/static/src/css/snippets.css @@ -42,8 +42,9 @@ border: 0; } #oe_snippets .tab-content { + margin-right: 120px; display: inline-block; - white-space: nowrap; + white-space: normal; background: black; margin-right: 60px; } @@ -68,7 +69,7 @@ } .oe_snippet { - float: left; + display: inline-block; vertical-align: top; width: 93px; margin-left: 1px; diff --git a/addons/website/static/src/css/snippets.sass b/addons/website/static/src/css/snippets.sass index 6aba0d51daf..0440b6f634c 100644 --- a/addons/website/static/src/css/snippets.sass +++ b/addons/website/static/src/css/snippets.sass @@ -39,8 +39,9 @@ .pill-content border: 0 .tab-content + margin-right: 120px display: inline-block - white-space: nowrap + white-space: normal background: black margin-right: 60px > div @@ -57,7 +58,7 @@ @include transform-origin(50%) .oe_snippet - float: left + display: inline-block vertical-align: top width: 93px margin-left: 1px diff --git a/addons/website_crm/controllers/main.py b/addons/website_crm/controllers/main.py index 02e700ae5bc..76f917ae398 100644 --- a/addons/website_crm/controllers/main.py +++ b/addons/website_crm/controllers/main.py @@ -28,7 +28,8 @@ class contactus(http.Controller): def create_lead(self, request, values, kwargs): """ Allow to be overrided """ - return request.registry['crm.lead'].create(request.cr, SUPERUSER_ID, values, request.context) + cr, context = request.cr, request.context + return request.registry['crm.lead'].create(cr, SUPERUSER_ID, values, context=dict(context, mail_create_nosubscribe=True)) def preRenderThanks(self, values, kwargs): """ Allow to be overrided """ @@ -76,11 +77,8 @@ class contactus(http.Controller): values = dict(values, error=error, kwargs=kwargs.items()) return request.website.render(kwargs.get("view_from", "website.contactus"), values) - try: - values['medium_id'] = request.registry['ir.model.data'].get_object_reference(request.cr, SUPERUSER_ID, 'crm', 'crm_tracking_medium_website')[1] - values['team_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'website.salesteam_website_sales') - except ValueError: - pass + values['medium_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'crm.crm_medium_website') + values['team_id'] = request.registry['ir.model.data'].xmlid_to_res_id(request.cr, SUPERUSER_ID, 'website.salesteam_website_sales') # description is required, so it is always already initialized if post_description: diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index b8fe561d4a5..63200dd51f2 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -48,6 +48,7 @@ class Forum(models.Model): faq = fields.Html('Guidelines', default=_get_default_faq, translate=True) description = fields.Html( 'Description', + translate=True, default='<p> This community is for professionals and enthusiasts of our products and services.' 'Share and discuss the best content and new marketing ideas,' 'build your professional profile and become a better marketer together.</p>') @@ -264,6 +265,15 @@ class Post(models.Model): def _get_post_karma_rights(self): user = self.env.user + def name_get(self, cr, uid, ids, context=None): + result = [] + for post in self.browse(cr, uid, ids, context=context): + if post.parent_id and not post.name: + result.append((post.id, '%s (%s)' % (post.parent_id.name, post.id))) + else: + result.append((post.id, '%s' % (post.name))) + return result + self.karma_accept = self.parent_id and self.parent_id.create_uid.id == self._uid and self.forum_id.karma_answer_accept_own or self.forum_id.karma_answer_accept_all self.karma_edit = self.create_uid.id == self._uid and self.forum_id.karma_edit_own or self.forum_id.karma_edit_all self.karma_close = self.create_uid.id == self._uid and self.forum_id.karma_close_own or self.forum_id.karma_close_all diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml index 421d76e37e2..916ddeb5d0c 100644 --- a/addons/website_forum/views/forum.xml +++ b/addons/website_forum/views/forum.xml @@ -29,6 +29,7 @@ <h1> <field name="name"/> </h1> + <field name="description"/> <notebook> <page string="Options"> <group string="Post Types"> diff --git a/addons/website_mail/tests/__init__.py b/addons/website_mail/tests/__init__.py index fb06fbc2f3e..c6311ba9f30 100644 --- a/addons/website_mail/tests/__init__.py +++ b/addons/website_mail/tests/__init__.py @@ -21,6 +21,3 @@ import test_controllers -checks = [ - test_controllers, -] diff --git a/addons/website_project_issue/tests/__init__.py b/addons/website_project_issue/tests/__init__.py index 93963ea4559..deb30d89953 100644 --- a/addons/website_project_issue/tests/__init__.py +++ b/addons/website_project_issue/tests/__init__.py @@ -21,6 +21,3 @@ from . import test_access_rights -checks = [ - test_access_rights, -] diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 6ada56a55c5..c8f4e29a6f9 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -144,8 +144,9 @@ class website_sale(http.Controller): domain = request.website.sale_product_domain() if search: - domain += ['|', '|', '|', ('name', 'ilike', search), ('description', 'ilike', search), - ('description_sale', 'ilike', search), ('product_variant_ids.default_code', 'ilike', search)] + for srch in search.split(" "): + domain += ['|', '|', '|', ('name', 'ilike', srch), ('description', 'ilike', srch), + ('description_sale', 'ilike', srch), ('product_variant_ids.default_code', 'ilike', srch)] if category: domain += [('public_categ_ids', 'child_of', int(category))] diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 1dbe4b2eb5c..255789ae9c3 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -47,15 +47,14 @@ class product_public_category(osv.osv): ] def name_get(self, cr, uid, ids, context=None): - if not len(ids): - return [] - reads = self.read(cr, uid, ids, ['name','parent_id'], context=context) res = [] - for record in reads: - name = record['name'] - if record['parent_id']: - name = record['parent_id'][1]+' / '+name - res.append((record['id'], name)) + for cat in self.browse(cr, uid, ids, context=context): + names = [cat.name] + pcat = cat.parent_id + while pcat: + names.append(pcat.name) + pcat = pcat.parent_id + res.append((cat.id, ' / '.join(reversed(names)))) return res def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): diff --git a/addons/website_sale/static/src/js/website_sale_payment.js b/addons/website_sale/static/src/js/website_sale_payment.js index ca61e0655d9..8ca1b85f907 100644 --- a/addons/website_sale/static/src/js/website_sale_payment.js +++ b/addons/website_sale/static/src/js/website_sale_payment.js @@ -10,7 +10,7 @@ $(document).ready(function () { .find("input[name='acquirer']:checked").click(); // When clicking on payment button: create the tx using json then continue to the acquirer - $payment.on("click", 'button[type="submit"]', function (ev) { + $payment.on("click", 'button[type="submit"],button[name="submit"]', function (ev) { ev.preventDefault(); ev.stopPropagation(); var $form = $(ev.currentTarget).parents('form'); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 19f7eabbd1f..42670547b20 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -748,7 +748,7 @@ </xpath> </template> -<template id="reduction_code" inherit_id="website_sale.cart" active="False" customize_show="True" name="Reduction Code"> +<template id="reduction_code" inherit_id="website_sale.cart" active="False" customize_show="True" name="Coupon Code"> <xpath expr="//div[@id='right_column']" position="inside"> <h4>Coupon Code</h4> <p> diff --git a/addons/website_twitter/static/src/js/website.twitter.animation.js b/addons/website_twitter/static/src/js/website.twitter.animation.js index 62d62cc5e74..10d9820ee30 100644 --- a/addons/website_twitter/static/src/js/website.twitter.animation.js +++ b/addons/website_twitter/static/src/js/website.twitter.animation.js @@ -17,9 +17,6 @@ if (url) { window.open(url, '_blank'); } - else { - debugger; - } } }); $("<center><div><img src='/website_twitter/static/src/img/loadtweet.gif'></div></center>").appendTo(timeline); diff --git a/doc/_themes/odoodoc/layout.html b/doc/_themes/odoodoc/layout.html index b01eaf05216..af62f44bbb4 100644 --- a/doc/_themes/odoodoc/layout.html +++ b/doc/_themes/odoodoc/layout.html @@ -6,7 +6,6 @@ '_static/bootstrap/js/tooltip.js', '_static/bootstrap/js/collapse.js', '_static/bootstrap/js/dropdown.js', - '_static/zeroclipboard-2.1.6/ZeroClipboard.min.js', '_static/app.js' ] %} diff --git a/doc/_themes/odoodoc/static/app.js b/doc/_themes/odoodoc/static/app.js index a0d7376530a..208a29dcf42 100644 --- a/doc/_themes/odoodoc/static/app.js +++ b/doc/_themes/odoodoc/static/app.js @@ -25,40 +25,6 @@ $(function () { }); }, 100); - /* - for clipboard: - * add per-language setup code to document, hidden - * adds button to each switchable language block except when they're setup - stuff because fuck'em - * per-language, add clipboard hook to prefix with setup bit on-copy - * setup bit is... ? - * actually not all blocks because we don't want to add the setup bits to - the setup bits, so that's kinda shit - */ - - document.addEventListener('copy', copyCode); - - ZeroClipboard.config({ - swfPath: '../_static/zeroclipboard-2.1.6/ZeroClipboard.swf', - flashLoadTimeout: 3e3, - }); - var zc = new ZeroClipboard(); - zc.on('ready', function () { - var $highlighted = $('.switchable:not(.setup) .highlight').addClass('with-btn-clipboard'); - var $clipboard_buttons = - $('<button type="button" class="btn-clipboard">Copy</button>') - .on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () { - $(this).removeClass('active'); - }) - .prependTo($highlighted); - zc.clip($clipboard_buttons); - }); - zc.on('copy', function (e) { - // yellow flash - $(e.target).addClass('active'); - copyCode(e); - }); - // stripe page stuff if ($('div.document-super').hasClass('stripe')) { (function () { // iterate on highlighted PL blocks (but not results because that'd @@ -95,38 +61,31 @@ $(function () { sheet.insertRule(sel + '{' + content + '}', lastIndex); }); $switcher.affix(); - })(); } + $('<button type="button" class="btn-show-setup">Toggle Setup Code</button>') + .prependTo('.switchable:not(.setup) .highlight'); + $(document).on('click', '.btn-show-setup', function (e) { + var $target = $(e.target); + var target = $target.closest('.switchable:not(.setup)').get(0); + // not in a switchable (???) + if (!target) { return; } + var lang = getHighlightLanguage(target); + if (!lang) { + // switchable without highlight (e.g. language-specific notes), + // don't munge + return; + } - function copyCode(e) { - // works for both C-c and "Copy" button if copy button is injected - // inside highlighted code section - var target = $(e.target).closest('.switchable:not(.setup)').get(0); - // not in a switchable - if (!target) { return; } - var lang = getHighlightLanguage(target); - if (!lang) { - // switchable without highlight (e.g. language-specific notes), - // don't munge - return; - } - - // get generic setup code - var setup_selector = '.setupcode.highlight-' + lang + ' pre'; - var setup = document.querySelector(setup_selector).textContent; - - // prepend setup code to current snippet, get all of current snippet - // in case only part of it was selected. Ensure we don't get e.g. - // button text around snippet itself - var data = setup + '\n' + target.querySelector('pre').textContent; - // sane browsers & ZeroClipboard - e.clipboardData.setData('text/plain', data); - // MSIE - e.clipboardData.setData('Text', data); + var $setup_code = $target.prev(); + if ($setup_code.length) { + // remove existing setup code + $setup_code.remove(); + } else { + $('.setupcode.highlight-' + lang + ' pre').clone().insertBefore($target); + } + }); + })(); } - // no preventDefault on ZC event - e.preventDefault && e.preventDefault(); - } /** * @param {Node} node highlight node to get the language of * @returns {String|null} either the highlight language or null diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css index 12ffb135f6f..7fc2d3dcb95 100644 --- a/doc/_themes/odoodoc/static/style.css +++ b/doc/_themes/odoodoc/static/style.css @@ -4076,7 +4076,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } } .navbar-default { - background-color: #ffffff; + background-color: white; border-color: 0; } .navbar-default .navbar-brand { @@ -6663,49 +6663,43 @@ div.section > h2 { .admonition.exercise > .admonition-title { color: #555555; } -/* - * Code snippets - * - * Generated via Pygments - */ +.highlight { + position: relative; + color: #333333; + background: #eeeeee none !important; + border: 1px solid #777777; + border-radius: 4px 0 4px 4px; + margin: 10px 0; +} .highlight pre { - padding: 4px; + padding: 0; + margin: 10px; font-size: 75%; word-break: normal; word-wrap: normal; + background: none; + border: none; } -.switchable .highlight.with-btn-clipboard pre { - padding-right: 50px; -} -/* - * ZeroClipboard styles - */ -.highlight { - position: relative; +.highlight:hover .btn-show-setup { + display: block; } -.btn-clipboard { +.highlight .btn-show-setup { + display: none; position: absolute; - top: 0; + bottom: 100%; right: 0; z-index: 3; - display: block; padding: 5px 8px; font-size: 12px; - color: #777; + color: #777777; cursor: pointer; - background-color: #fff; - border: 1px solid #e1e1e8; - border-radius: 0 4px 0 4px; + border: 1px solid #777777; + border-radius: 4px 4px 0 0; + background: inherit; } -.btn-clipboard:focus { +.highlight .btn-show-setup:focus { outline: none; } -.btn-clipboard.active { - background-color: #ffffad; - -webkit-transition: background-color 0.5s linear; - -o-transition: background-color 0.5s linear; - transition: background-color 0.5s linear; -} img.align-center { display: block; margin: 0 auto; @@ -6843,20 +6837,14 @@ td.field-body > ul { border-bottom-color: #777777; border-right-color: #777777; } - .stripe .section > .force-right pre, - .stripe .section > [class*=highlight] pre { - padding: 0; - margin: 10px; - background: none; - border: none; - } .stripe .section > .force-right .highlight, .stripe .section > [class*=highlight] .highlight { border-color: #555555; border-style: solid; border-width: 1px 0; + border-radius: 0; color: #cccccc; - background: none; + background: none !important; /* Line Numbers */ /* Comment */ /* Error */ @@ -7243,14 +7231,12 @@ td.field-body > ul { .stripe .highlight pre { white-space: pre-wrap; } - .stripe .btn-clipboard { + .stripe .btn-show-setup { background-color: transparent; color: #eeeeee; border-color: #777777; - border-top: none; - border-top-right-radius: 0; } - .stripe .btn-clipboard.active { + .stripe .btn-show-setup.active { background-color: #555555; -webkit-transition: background-color 0.5s linear; -o-transition: background-color 0.5s linear; diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less index d3703999414..480e43c5cb8 100644 --- a/doc/_themes/odoodoc/static/style.less +++ b/doc/_themes/odoodoc/static/style.less @@ -465,50 +465,50 @@ div.section > h2 { } } -/* - * Code snippets - * - * Generated via Pygments - */ -.highlight pre { - padding: 4px; - font-size: 75%; - // don't break lines within words - word-break: normal; - word-wrap: normal; -} -.switchable .highlight.with-btn-clipboard pre { - // avoid copy button overlapping with even long lines - padding-right: 50px; -} - -/* - * ZeroClipboard styles - */ .highlight { position: relative; -} -.btn-clipboard { - position: absolute; - top: 0; - right: 0; - z-index: 3; - display: block; - padding: 5px 8px; - font-size: 12px; - color: #777; - cursor: pointer; - background-color: #fff; - border: 1px solid #e1e1e8; - border-radius: 0 4px 0 4px; - &:focus { - outline: none; + // move styling from pre to highlight so can have multiple pre elements in + // a single "code block" for ease of adding and removing setup code &al + color: @gray-dark; + // force override of pygments yellow background background + background: @gray-lighter none !important; + border: 1px solid @gray-light; + border-radius: @border-radius-base 0 @border-radius-base @border-radius-base; + margin: 10px 0; + + pre { + padding: 0; + margin: 10px; + font-size: 75%; + // don't break lines within words + word-break: normal; + word-wrap: normal; + + background: none; + border: none; } - &.active { - background-color: #ffffad; - .transition(background-color .5s linear); + &:hover .btn-show-setup { + display: block; + } + .btn-show-setup { + display: none; + position: absolute; + bottom: 100%; + right: 0; + z-index: 3; + padding: 5px 8px; + font-size: 12px; + color: @gray-light; + cursor: pointer; + border: 1px solid @gray-light; + border-radius: @border-radius-base @border-radius-base 0 0; + background: inherit; + + &:focus { + outline: none; + } } } @@ -638,22 +638,14 @@ td.field-body { border-bottom-color: @separator-right; border-right-color: @separator-right; } - pre { - // padding leads to overflow scrollbar in safari for no reason I understand, - // margin does not have that issue - padding: 0; - margin: 10px; - - background: none; - border: none; - } .highlight { border-color: @gray; border-style: solid; border-width: 1px 0; + border-radius: 0; // solarized-ish from https://gist.github.com/qguv/7936275 color: @color-right-code; - background: none; + background: none !important; .lineno { color: #586e75 } /* Line Numbers */ .c { color: #586e75 } /* Comment */ .err { color: @color-right-code } /* Error */ @@ -761,12 +753,10 @@ td.field-body { white-space: pre-wrap; } - .btn-clipboard { + .btn-show-setup { background-color: transparent; color: @color-right; border-color: @separator-right; - border-top: none; - border-top-right-radius: 0; &.active { background-color: @gray; diff --git a/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.min.js b/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.min.js deleted file mode 100644 index 365737f559a..00000000000 --- a/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * ZeroClipboard - * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. - * Copyright (c) 2014 Jon Rohan, James M. Greene - * Licensed MIT - * http://zeroclipboard.org/ - * v2.1.6 - */ -!function(a,b){"use strict";var c,d,e=a,f=e.document,g=e.navigator,h=e.setTimeout,i=e.encodeURIComponent,j=e.ActiveXObject,k=e.Error,l=e.Number.parseInt||e.parseInt,m=e.Number.parseFloat||e.parseFloat,n=e.Number.isNaN||e.isNaN,o=e.Math.round,p=e.Date.now,q=e.Object.keys,r=e.Object.defineProperty,s=e.Object.prototype.hasOwnProperty,t=e.Array.prototype.slice,u=function(){var a=function(a){return a};if("function"==typeof e.wrap&&"function"==typeof e.unwrap)try{var b=f.createElement("div"),c=e.unwrap(b);1===b.nodeType&&c&&1===c.nodeType&&(a=e.unwrap)}catch(d){}return a}(),v=function(a){return t.call(a,0)},w=function(){var a,c,d,e,f,g,h=v(arguments),i=h[0]||{};for(a=1,c=h.length;c>a;a++)if(null!=(d=h[a]))for(e in d)s.call(d,e)&&(f=i[e],g=d[e],i!==g&&g!==b&&(i[e]=g));return i},x=function(a){var b,c,d,e;if("object"!=typeof a||null==a)b=a;else if("number"==typeof a.length)for(b=[],c=0,d=a.length;d>c;c++)s.call(a,c)&&(b[c]=x(a[c]));else{b={};for(e in a)s.call(a,e)&&(b[e]=x(a[e]))}return b},y=function(a,b){for(var c={},d=0,e=b.length;e>d;d++)b[d]in a&&(c[b[d]]=a[b[d]]);return c},z=function(a,b){var c={};for(var d in a)-1===b.indexOf(d)&&(c[d]=a[d]);return c},A=function(a){if(a)for(var b in a)s.call(a,b)&&delete a[b];return a},B=function(a,b){if(a&&1===a.nodeType&&a.ownerDocument&&b&&(1===b.nodeType&&b.ownerDocument&&b.ownerDocument===a.ownerDocument||9===b.nodeType&&!b.ownerDocument&&b===a.ownerDocument))do{if(a===b)return!0;a=a.parentNode}while(a);return!1},C=function(a){var b;return"string"==typeof a&&a&&(b=a.split("#")[0].split("?")[0],b=a.slice(0,a.lastIndexOf("/")+1)),b},D=function(a){var b,c;return"string"==typeof a&&a&&(c=a.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]?b=c[1]:(c=a.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]&&(b=c[1]))),b},E=function(){var a,b;try{throw new k}catch(c){b=c}return b&&(a=b.sourceURL||b.fileName||D(b.stack)),a},F=function(){var a,c,d;if(f.currentScript&&(a=f.currentScript.src))return a;if(c=f.getElementsByTagName("script"),1===c.length)return c[0].src||b;if("readyState"in c[0])for(d=c.length;d--;)if("interactive"===c[d].readyState&&(a=c[d].src))return a;return"loading"===f.readyState&&(a=c[c.length-1].src)?a:(a=E())?a:b},G=function(){var a,c,d,e=f.getElementsByTagName("script");for(a=e.length;a--;){if(!(d=e[a].src)){c=null;break}if(d=C(d),null==c)c=d;else if(c!==d){c=null;break}}return c||b},H=function(){var a=C(F())||G()||"";return a+"ZeroClipboard.swf"},I={bridge:null,version:"0.0.0",pluginType:"unknown",disabled:null,outdated:null,unavailable:null,deactivated:null,overdue:null,ready:null},J="11.0.0",K={},L={},M=null,N={ready:"Flash communication is established",error:{"flash-disabled":"Flash is disabled or not installed","flash-outdated":"Flash is too outdated to support ZeroClipboard","flash-unavailable":"Flash is unable to communicate bidirectionally with JavaScript","flash-deactivated":"Flash is too outdated for your browser and/or is configured as click-to-activate","flash-overdue":"Flash communication was established but NOT within the acceptable time limit"}},O={swfPath:H(),trustedDomains:a.location.host?[a.location.host]:[],cacheBust:!0,forceEnhancedClipboard:!1,flashLoadTimeout:3e4,autoActivate:!0,bubbleEvents:!0,containerId:"global-zeroclipboard-html-bridge",containerClass:"global-zeroclipboard-container",swfObjectId:"global-zeroclipboard-flash-bridge",hoverClass:"zeroclipboard-is-hover",activeClass:"zeroclipboard-is-active",forceHandCursor:!1,title:null,zIndex:999999999},P=function(a){if("object"==typeof a&&null!==a)for(var b in a)if(s.call(a,b))if(/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(b))O[b]=a[b];else if(null==I.bridge)if("containerId"===b||"swfObjectId"===b){if(!cb(a[b]))throw new Error("The specified `"+b+"` value is not valid as an HTML4 Element ID");O[b]=a[b]}else O[b]=a[b];{if("string"!=typeof a||!a)return x(O);if(s.call(O,a))return O[a]}},Q=function(){return{browser:y(g,["userAgent","platform","appName"]),flash:z(I,["bridge"]),zeroclipboard:{version:Fb.version,config:Fb.config()}}},R=function(){return!!(I.disabled||I.outdated||I.unavailable||I.deactivated)},S=function(a,b){var c,d,e,f={};if("string"==typeof a&&a)e=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Fb.on(c,a[c]);if(e&&e.length){for(c=0,d=e.length;d>c;c++)a=e[c].replace(/^on/,""),f[a]=!0,K[a]||(K[a]=[]),K[a].push(b);if(f.ready&&I.ready&&Fb.emit({type:"ready"}),f.error){var g=["disabled","outdated","unavailable","deactivated","overdue"];for(c=0,d=g.length;d>c;c++)if(I[g[c]]===!0){Fb.emit({type:"error",name:"flash-"+g[c]});break}}}return Fb},T=function(a,b){var c,d,e,f,g;if(0===arguments.length)f=q(K);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Fb.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=K[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return Fb},U=function(a){var b;return b="string"==typeof a&&a?x(K[a])||null:x(K)},V=function(a){var b,c,d;return a=db(a),a&&!jb(a)?"ready"===a.type&&I.overdue===!0?Fb.emit({type:"error",name:"flash-overdue"}):(b=w({},a),ib.call(this,b),"copy"===a.type&&(d=pb(L),c=d.data,M=d.formatMap),c):void 0},W=function(){if("boolean"!=typeof I.ready&&(I.ready=!1),!Fb.isFlashUnusable()&&null===I.bridge){var a=O.flashLoadTimeout;"number"==typeof a&&a>=0&&h(function(){"boolean"!=typeof I.deactivated&&(I.deactivated=!0),I.deactivated===!0&&Fb.emit({type:"error",name:"flash-deactivated"})},a),I.overdue=!1,nb()}},X=function(){Fb.clearData(),Fb.blur(),Fb.emit("destroy"),ob(),Fb.off()},Y=function(a,b){var c;if("object"==typeof a&&a&&"undefined"==typeof b)c=a,Fb.clearData();else{if("string"!=typeof a||!a)return;c={},c[a]=b}for(var d in c)"string"==typeof d&&d&&s.call(c,d)&&"string"==typeof c[d]&&c[d]&&(L[d]=c[d])},Z=function(a){"undefined"==typeof a?(A(L),M=null):"string"==typeof a&&s.call(L,a)&&delete L[a]},$=function(a){return"undefined"==typeof a?x(L):"string"==typeof a&&s.call(L,a)?L[a]:void 0},_=function(a){if(a&&1===a.nodeType){c&&(xb(c,O.activeClass),c!==a&&xb(c,O.hoverClass)),c=a,wb(a,O.hoverClass);var b=a.getAttribute("title")||O.title;if("string"==typeof b&&b){var d=mb(I.bridge);d&&d.setAttribute("title",b)}var e=O.forceHandCursor===!0||"pointer"===yb(a,"cursor");Cb(e),Bb()}},ab=function(){var a=mb(I.bridge);a&&(a.removeAttribute("title"),a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.top="1px"),c&&(xb(c,O.hoverClass),xb(c,O.activeClass),c=null)},bb=function(){return c||null},cb=function(a){return"string"==typeof a&&a&&/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(a)},db=function(a){var b;if("string"==typeof a&&a?(b=a,a={}):"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(b=a.type),b){!a.target&&/^(copy|aftercopy|_click)$/.test(b.toLowerCase())&&(a.target=d),w(a,{type:b.toLowerCase(),target:a.target||c||null,relatedTarget:a.relatedTarget||null,currentTarget:I&&I.bridge||null,timeStamp:a.timeStamp||p()||null});var e=N[a.type];return"error"===a.type&&a.name&&e&&(e=e[a.name]),e&&(a.message=e),"ready"===a.type&&w(a,{target:null,version:I.version}),"error"===a.type&&(/^flash-(disabled|outdated|unavailable|deactivated|overdue)$/.test(a.name)&&w(a,{target:null,minimumVersion:J}),/^flash-(outdated|unavailable|deactivated|overdue)$/.test(a.name)&&w(a,{version:I.version})),"copy"===a.type&&(a.clipboardData={setData:Fb.setData,clearData:Fb.clearData}),"aftercopy"===a.type&&(a=qb(a,M)),a.target&&!a.relatedTarget&&(a.relatedTarget=eb(a.target)),a=fb(a)}},eb=function(a){var b=a&&a.getAttribute&&a.getAttribute("data-clipboard-target");return b?f.getElementById(b):null},fb=function(a){if(a&&/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)){var c=a.target,d="_mouseover"===a.type&&a.relatedTarget?a.relatedTarget:b,g="_mouseout"===a.type&&a.relatedTarget?a.relatedTarget:b,h=Ab(c),i=e.screenLeft||e.screenX||0,j=e.screenTop||e.screenY||0,k=f.body.scrollLeft+f.documentElement.scrollLeft,l=f.body.scrollTop+f.documentElement.scrollTop,m=h.left+("number"==typeof a._stageX?a._stageX:0),n=h.top+("number"==typeof a._stageY?a._stageY:0),o=m-k,p=n-l,q=i+o,r=j+p,s="number"==typeof a.movementX?a.movementX:0,t="number"==typeof a.movementY?a.movementY:0;delete a._stageX,delete a._stageY,w(a,{srcElement:c,fromElement:d,toElement:g,screenX:q,screenY:r,pageX:m,pageY:n,clientX:o,clientY:p,x:o,y:p,movementX:s,movementY:t,offsetX:0,offsetY:0,layerX:0,layerY:0})}return a},gb=function(a){var b=a&&"string"==typeof a.type&&a.type||"";return!/^(?:(?:before)?copy|destroy)$/.test(b)},hb=function(a,b,c,d){d?h(function(){a.apply(b,c)},0):a.apply(b,c)},ib=function(a){if("object"==typeof a&&a&&a.type){var b=gb(a),c=K["*"]||[],d=K[a.type]||[],f=c.concat(d);if(f&&f.length){var g,h,i,j,k,l=this;for(g=0,h=f.length;h>g;g++)i=f[g],j=l,"string"==typeof i&&"function"==typeof e[i]&&(i=e[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=w({},a),hb(i,j,[k],b))}return this}},jb=function(a){var b=a.target||c||null,e="swf"===a._source;delete a._source;var f=["flash-disabled","flash-outdated","flash-unavailable","flash-deactivated","flash-overdue"];switch(a.type){case"error":-1!==f.indexOf(a.name)&&w(I,{disabled:"flash-disabled"===a.name,outdated:"flash-outdated"===a.name,unavailable:"flash-unavailable"===a.name,deactivated:"flash-deactivated"===a.name,overdue:"flash-overdue"===a.name,ready:!1});break;case"ready":var g=I.deactivated===!0;w(I,{disabled:!1,outdated:!1,unavailable:!1,deactivated:!1,overdue:g,ready:!g});break;case"beforecopy":d=b;break;case"copy":var h,i,j=a.relatedTarget;!L["text/html"]&&!L["text/plain"]&&j&&(i=j.value||j.outerHTML||j.innerHTML)&&(h=j.value||j.textContent||j.innerText)?(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",h),i!==h&&a.clipboardData.setData("text/html",i)):!L["text/plain"]&&a.target&&(h=a.target.getAttribute("data-clipboard-text"))&&(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",h));break;case"aftercopy":Fb.clearData(),b&&b!==vb()&&b.focus&&b.focus();break;case"_mouseover":Fb.focus(b),O.bubbleEvents===!0&&e&&(b&&b!==a.relatedTarget&&!B(a.relatedTarget,b)&&kb(w({},a,{type:"mouseenter",bubbles:!1,cancelable:!1})),kb(w({},a,{type:"mouseover"})));break;case"_mouseout":Fb.blur(),O.bubbleEvents===!0&&e&&(b&&b!==a.relatedTarget&&!B(a.relatedTarget,b)&&kb(w({},a,{type:"mouseleave",bubbles:!1,cancelable:!1})),kb(w({},a,{type:"mouseout"})));break;case"_mousedown":wb(b,O.activeClass),O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}));break;case"_mouseup":xb(b,O.activeClass),O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}));break;case"_click":d=null,O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}));break;case"_mousemove":O.bubbleEvents===!0&&e&&kb(w({},a,{type:a.type.slice(1)}))}return/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)?!0:void 0},kb=function(a){if(a&&"string"==typeof a.type&&a){var b,c=a.target||null,d=c&&c.ownerDocument||f,g={view:d.defaultView||e,canBubble:!0,cancelable:!0,detail:"click"===a.type?1:0,button:"number"==typeof a.which?a.which-1:"number"==typeof a.button?a.button:d.createEvent?0:1},h=w(g,a);c&&d.createEvent&&c.dispatchEvent&&(h=[h.type,h.canBubble,h.cancelable,h.view,h.detail,h.screenX,h.screenY,h.clientX,h.clientY,h.ctrlKey,h.altKey,h.shiftKey,h.metaKey,h.button,h.relatedTarget],b=d.createEvent("MouseEvents"),b.initMouseEvent&&(b.initMouseEvent.apply(b,h),b._source="js",c.dispatchEvent(b)))}},lb=function(){var a=f.createElement("div");return a.id=O.containerId,a.className=O.containerClass,a.style.position="absolute",a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px",a.style.zIndex=""+Db(O.zIndex),a},mb=function(a){for(var b=a&&a.parentNode;b&&"OBJECT"===b.nodeName&&b.parentNode;)b=b.parentNode;return b||null},nb=function(){var a,b=I.bridge,c=mb(b);if(!b){var d=ub(e.location.host,O),g="never"===d?"none":"all",h=sb(O),i=O.swfPath+rb(O.swfPath,O);c=lb();var j=f.createElement("div");c.appendChild(j),f.body.appendChild(c);var k=f.createElement("div"),l="activex"===I.pluginType;k.innerHTML='<object id="'+O.swfObjectId+'" name="'+O.swfObjectId+'" width="100%" height="100%" '+(l?'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"':'type="application/x-shockwave-flash" data="'+i+'"')+">"+(l?'<param name="movie" value="'+i+'"/>':"")+'<param name="allowScriptAccess" value="'+d+'"/><param name="allowNetworking" value="'+g+'"/><param name="menu" value="false"/><param name="wmode" value="transparent"/><param name="flashvars" value="'+h+'"/></object>',b=k.firstChild,k=null,u(b).ZeroClipboard=Fb,c.replaceChild(b,j)}return b||(b=f[O.swfObjectId],b&&(a=b.length)&&(b=b[a-1]),!b&&c&&(b=c.firstChild)),I.bridge=b||null,b},ob=function(){var a=I.bridge;if(a){var b=mb(a);b&&("activex"===I.pluginType&&"readyState"in a?(a.style.display="none",function c(){if(4===a.readyState){for(var d in a)"function"==typeof a[d]&&(a[d]=null);a.parentNode&&a.parentNode.removeChild(a),b.parentNode&&b.parentNode.removeChild(b)}else h(c,10)}()):(a.parentNode&&a.parentNode.removeChild(a),b.parentNode&&b.parentNode.removeChild(b))),I.ready=null,I.bridge=null,I.deactivated=null}},pb=function(a){var b={},c={};if("object"==typeof a&&a){for(var d in a)if(d&&s.call(a,d)&&"string"==typeof a[d]&&a[d])switch(d.toLowerCase()){case"text/plain":case"text":case"air:text":case"flash:text":b.text=a[d],c.text=d;break;case"text/html":case"html":case"air:html":case"flash:html":b.html=a[d],c.html=d;break;case"application/rtf":case"text/rtf":case"rtf":case"richtext":case"air:rtf":case"flash:rtf":b.rtf=a[d],c.rtf=d}return{data:b,formatMap:c}}},qb=function(a,b){if("object"!=typeof a||!a||"object"!=typeof b||!b)return a;var c={};for(var d in a)if(s.call(a,d)){if("success"!==d&&"data"!==d){c[d]=a[d];continue}c[d]={};var e=a[d];for(var f in e)f&&s.call(e,f)&&s.call(b,f)&&(c[d][b[f]]=e[f])}return c},rb=function(a,b){var c=null==b||b&&b.cacheBust===!0;return c?(-1===a.indexOf("?")?"?":"&")+"noCache="+p():""},sb=function(a){var b,c,d,f,g="",h=[];if(a.trustedDomains&&("string"==typeof a.trustedDomains?f=[a.trustedDomains]:"object"==typeof a.trustedDomains&&"length"in a.trustedDomains&&(f=a.trustedDomains)),f&&f.length)for(b=0,c=f.length;c>b;b++)if(s.call(f,b)&&f[b]&&"string"==typeof f[b]){if(d=tb(f[b]),!d)continue;if("*"===d){h.length=0,h.push(d);break}h.push.apply(h,[d,"//"+d,e.location.protocol+"//"+d])}return h.length&&(g+="trustedOrigins="+i(h.join(","))),a.forceEnhancedClipboard===!0&&(g+=(g?"&":"")+"forceEnhancedClipboard=true"),"string"==typeof a.swfObjectId&&a.swfObjectId&&(g+=(g?"&":"")+"swfObjectId="+i(a.swfObjectId)),g},tb=function(a){if(null==a||""===a)return null;if(a=a.replace(/^\s+|\s+$/g,""),""===a)return null;var b=a.indexOf("//");a=-1===b?a:a.slice(b+2);var c=a.indexOf("/");return a=-1===c?a:-1===b||0===c?null:a.slice(0,c),a&&".swf"===a.slice(-4).toLowerCase()?null:a||null},ub=function(){var a=function(a){var b,c,d,e=[];if("string"==typeof a&&(a=[a]),"object"!=typeof a||!a||"number"!=typeof a.length)return e;for(b=0,c=a.length;c>b;b++)if(s.call(a,b)&&(d=tb(a[b]))){if("*"===d){e.length=0,e.push("*");break}-1===e.indexOf(d)&&e.push(d)}return e};return function(b,c){var d=tb(c.swfPath);null===d&&(d=b);var e=a(c.trustedDomains),f=e.length;if(f>0){if(1===f&&"*"===e[0])return"always";if(-1!==e.indexOf(b))return 1===f&&b===d?"sameDomain":"always"}return"never"}}(),vb=function(){try{return f.activeElement}catch(a){return null}},wb=function(a,b){if(!a||1!==a.nodeType)return a;if(a.classList)return a.classList.contains(b)||a.classList.add(b),a;if(b&&"string"==typeof b){var c=(b||"").split(/\s+/);if(1===a.nodeType)if(a.className){for(var d=" "+a.className+" ",e=a.className,f=0,g=c.length;g>f;f++)d.indexOf(" "+c[f]+" ")<0&&(e+=" "+c[f]);a.className=e.replace(/^\s+|\s+$/g,"")}else a.className=b}return a},xb=function(a,b){if(!a||1!==a.nodeType)return a;if(a.classList)return a.classList.contains(b)&&a.classList.remove(b),a;if("string"==typeof b&&b){var c=b.split(/\s+/);if(1===a.nodeType&&a.className){for(var d=(" "+a.className+" ").replace(/[\n\t]/g," "),e=0,f=c.length;f>e;e++)d=d.replace(" "+c[e]+" "," ");a.className=d.replace(/^\s+|\s+$/g,"")}}return a},yb=function(a,b){var c=e.getComputedStyle(a,null).getPropertyValue(b);return"cursor"!==b||c&&"auto"!==c||"A"!==a.nodeName?c:"pointer"},zb=function(){var a,b,c,d=1;return"function"==typeof f.body.getBoundingClientRect&&(a=f.body.getBoundingClientRect(),b=a.right-a.left,c=f.body.offsetWidth,d=o(b/c*100)/100),d},Ab=function(a){var b={left:0,top:0,width:0,height:0};if(a.getBoundingClientRect){var c,d,g,h=a.getBoundingClientRect();"pageXOffset"in e&&"pageYOffset"in e?(c=e.pageXOffset,d=e.pageYOffset):(g=zb(),c=o(f.documentElement.scrollLeft/g),d=o(f.documentElement.scrollTop/g));var i=f.documentElement.clientLeft||0,j=f.documentElement.clientTop||0;b.left=h.left+c-i,b.top=h.top+d-j,b.width="width"in h?h.width:h.right-h.left,b.height="height"in h?h.height:h.bottom-h.top}return b},Bb=function(){var a;if(c&&(a=mb(I.bridge))){var b=Ab(c);w(a.style,{width:b.width+"px",height:b.height+"px",top:b.top+"px",left:b.left+"px",zIndex:""+Db(O.zIndex)})}},Cb=function(a){I.ready===!0&&(I.bridge&&"function"==typeof I.bridge.setHandCursor?I.bridge.setHandCursor(a):I.ready=!1)},Db=function(a){if(/^(?:auto|inherit)$/.test(a))return a;var b;return"number"!=typeof a||n(a)?"string"==typeof a&&(b=Db(l(a,10))):b=a,"number"==typeof b?b:"auto"},Eb=function(a){function b(a){var b=a.match(/[\d]+/g);return b.length=3,b.join(".")}function c(a){return!!a&&(a=a.toLowerCase())&&(/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(a)||"chrome.plugin"===a.slice(-13))}function d(a){a&&(i=!0,a.version&&(l=b(a.version)),!l&&a.description&&(l=b(a.description)),a.filename&&(k=c(a.filename)))}var e,f,h,i=!1,j=!1,k=!1,l="";if(g.plugins&&g.plugins.length)e=g.plugins["Shockwave Flash"],d(e),g.plugins["Shockwave Flash 2.0"]&&(i=!0,l="2.0.0.11");else if(g.mimeTypes&&g.mimeTypes.length)h=g.mimeTypes["application/x-shockwave-flash"],e=h&&h.enabledPlugin,d(e);else if("undefined"!=typeof a){j=!0;try{f=new a("ShockwaveFlash.ShockwaveFlash.7"),i=!0,l=b(f.GetVariable("$version"))}catch(n){try{f=new a("ShockwaveFlash.ShockwaveFlash.6"),i=!0,l="6.0.21"}catch(o){try{f=new a("ShockwaveFlash.ShockwaveFlash"),i=!0,l=b(f.GetVariable("$version"))}catch(p){j=!1}}}}I.disabled=i!==!0,I.outdated=l&&m(l)<m(J),I.version=l||"0.0.0",I.pluginType=k?"pepper":j?"activex":i?"netscape":"unknown"};Eb(j);var Fb=function(){return this instanceof Fb?void("function"==typeof Fb._createClient&&Fb._createClient.apply(this,v(arguments))):new Fb};r(Fb,"version",{value:"2.1.6",writable:!1,configurable:!0,enumerable:!0}),Fb.config=function(){return P.apply(this,v(arguments))},Fb.state=function(){return Q.apply(this,v(arguments))},Fb.isFlashUnusable=function(){return R.apply(this,v(arguments))},Fb.on=function(){return S.apply(this,v(arguments))},Fb.off=function(){return T.apply(this,v(arguments))},Fb.handlers=function(){return U.apply(this,v(arguments))},Fb.emit=function(){return V.apply(this,v(arguments))},Fb.create=function(){return W.apply(this,v(arguments))},Fb.destroy=function(){return X.apply(this,v(arguments))},Fb.setData=function(){return Y.apply(this,v(arguments))},Fb.clearData=function(){return Z.apply(this,v(arguments))},Fb.getData=function(){return $.apply(this,v(arguments))},Fb.focus=Fb.activate=function(){return _.apply(this,v(arguments))},Fb.blur=Fb.deactivate=function(){return ab.apply(this,v(arguments))},Fb.activeElement=function(){return bb.apply(this,v(arguments))};var Gb=0,Hb={},Ib=0,Jb={},Kb={};w(O,{autoActivate:!0});var Lb=function(a){var b=this;b.id=""+Gb++,Hb[b.id]={instance:b,elements:[],handlers:{}},a&&b.clip(a),Fb.on("*",function(a){return b.emit(a)}),Fb.on("destroy",function(){b.destroy()}),Fb.create()},Mb=function(a,b){var c,d,e,f={},g=Hb[this.id]&&Hb[this.id].handlers;if("string"==typeof a&&a)e=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.on(c,a[c]);if(e&&e.length){for(c=0,d=e.length;d>c;c++)a=e[c].replace(/^on/,""),f[a]=!0,g[a]||(g[a]=[]),g[a].push(b);if(f.ready&&I.ready&&this.emit({type:"ready",client:this}),f.error){var h=["disabled","outdated","unavailable","deactivated","overdue"];for(c=0,d=h.length;d>c;c++)if(I[h[c]]){this.emit({type:"error",name:"flash-"+h[c],client:this});break}}}return this},Nb=function(a,b){var c,d,e,f,g,h=Hb[this.id]&&Hb[this.id].handlers;if(0===arguments.length)f=q(h);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)s.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=h[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return this},Ob=function(a){var b=null,c=Hb[this.id]&&Hb[this.id].handlers;return c&&(b="string"==typeof a&&a?c[a]?c[a].slice(0):[]:x(c)),b},Pb=function(a){if(Ub.call(this,a)){"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(a=w({},a));var b=w({},db(a),{client:this});Vb.call(this,b)}return this},Qb=function(a){a=Wb(a);for(var b=0;b<a.length;b++)if(s.call(a,b)&&a[b]&&1===a[b].nodeType){a[b].zcClippingId?-1===Jb[a[b].zcClippingId].indexOf(this.id)&&Jb[a[b].zcClippingId].push(this.id):(a[b].zcClippingId="zcClippingId_"+Ib++,Jb[a[b].zcClippingId]=[this.id],O.autoActivate===!0&&Xb(a[b]));var c=Hb[this.id]&&Hb[this.id].elements;-1===c.indexOf(a[b])&&c.push(a[b])}return this},Rb=function(a){var b=Hb[this.id];if(!b)return this;var c,d=b.elements;a="undefined"==typeof a?d.slice(0):Wb(a);for(var e=a.length;e--;)if(s.call(a,e)&&a[e]&&1===a[e].nodeType){for(c=0;-1!==(c=d.indexOf(a[e],c));)d.splice(c,1);var f=Jb[a[e].zcClippingId];if(f){for(c=0;-1!==(c=f.indexOf(this.id,c));)f.splice(c,1);0===f.length&&(O.autoActivate===!0&&Yb(a[e]),delete a[e].zcClippingId)}}return this},Sb=function(){var a=Hb[this.id];return a&&a.elements?a.elements.slice(0):[]},Tb=function(){this.unclip(),this.off(),delete Hb[this.id]},Ub=function(a){if(!a||!a.type)return!1;if(a.client&&a.client!==this)return!1;var b=Hb[this.id]&&Hb[this.id].elements,c=!!b&&b.length>0,d=!a.target||c&&-1!==b.indexOf(a.target),e=a.relatedTarget&&c&&-1!==b.indexOf(a.relatedTarget),f=a.client&&a.client===this;return d||e||f?!0:!1},Vb=function(a){if("object"==typeof a&&a&&a.type){var b=gb(a),c=Hb[this.id]&&Hb[this.id].handlers["*"]||[],d=Hb[this.id]&&Hb[this.id].handlers[a.type]||[],f=c.concat(d);if(f&&f.length){var g,h,i,j,k,l=this;for(g=0,h=f.length;h>g;g++)i=f[g],j=l,"string"==typeof i&&"function"==typeof e[i]&&(i=e[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=w({},a),hb(i,j,[k],b))}return this}},Wb=function(a){return"string"==typeof a&&(a=[]),"number"!=typeof a.length?[a]:a},Xb=function(a){if(a&&1===a.nodeType){var b=function(a){(a||(a=e.event))&&("js"!==a._source&&(a.stopImmediatePropagation(),a.preventDefault()),delete a._source)},c=function(c){(c||(c=e.event))&&(b(c),Fb.focus(a))};a.addEventListener("mouseover",c,!1),a.addEventListener("mouseout",b,!1),a.addEventListener("mouseenter",b,!1),a.addEventListener("mouseleave",b,!1),a.addEventListener("mousemove",b,!1),Kb[a.zcClippingId]={mouseover:c,mouseout:b,mouseenter:b,mouseleave:b,mousemove:b}}},Yb=function(a){if(a&&1===a.nodeType){var b=Kb[a.zcClippingId];if("object"==typeof b&&b){for(var c,d,e=["move","leave","enter","out","over"],f=0,g=e.length;g>f;f++)c="mouse"+e[f],d=b[c],"function"==typeof d&&a.removeEventListener(c,d,!1);delete Kb[a.zcClippingId]}}};Fb._createClient=function(){Lb.apply(this,v(arguments))},Fb.prototype.on=function(){return Mb.apply(this,v(arguments))},Fb.prototype.off=function(){return Nb.apply(this,v(arguments))},Fb.prototype.handlers=function(){return Ob.apply(this,v(arguments))},Fb.prototype.emit=function(){return Pb.apply(this,v(arguments))},Fb.prototype.clip=function(){return Qb.apply(this,v(arguments))},Fb.prototype.unclip=function(){return Rb.apply(this,v(arguments))},Fb.prototype.elements=function(){return Sb.apply(this,v(arguments))},Fb.prototype.destroy=function(){return Tb.apply(this,v(arguments))},Fb.prototype.setText=function(a){return Fb.setData("text/plain",a),this},Fb.prototype.setHtml=function(a){return Fb.setData("text/html",a),this},Fb.prototype.setRichText=function(a){return Fb.setData("application/rtf",a),this},Fb.prototype.setData=function(){return Fb.setData.apply(this,v(arguments)),this},Fb.prototype.clearData=function(){return Fb.clearData.apply(this,v(arguments)),this},Fb.prototype.getData=function(){return Fb.getData.apply(this,v(arguments))},"function"==typeof define&&define.amd?define(function(){return Fb}):"object"==typeof module&&module&&"object"==typeof module.exports&&module.exports?module.exports=Fb:a.ZeroClipboard=Fb}(function(){return this||window}()); diff --git a/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.swf b/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.swf Binary files differdeleted file mode 100644 index d4e2561b366..00000000000 --- a/doc/_themes/odoodoc/static/zeroclipboard-2.1.6/ZeroClipboard.swf +++ /dev/null diff --git a/doc/api_integration.rst b/doc/api_integration.rst index 6ee6871ff9f..ffcf3f9a029 100644 --- a/doc/api_integration.rst +++ b/doc/api_integration.rst @@ -1,8 +1,8 @@ :classes: stripe -=========== -Web Service -=========== +=============== +Web Service API +=============== Odoo is mostly extended internally via modules, but much of its features and all of its data is also available from the outside for external analysis or diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst index fcbc10e3721..0ac7ab8060d 100644 --- a/doc/howtos/backend.rst +++ b/doc/howtos/backend.rst @@ -74,31 +74,9 @@ option. most command-line options can also be set using :ref:`a configuration file <reference/cmdline/config>` -An Odoo module is declared by its :ref:`manifest <reference/module/manifest>`. It -is mandatory and contains a single python dictionary declaring various -metadata for the module: the module's name and description, list of Odoo -modules required for this one to work properly, references to data files, … - -The manifest's general structure is:: - - { - 'name': "MyModule", - 'version': '1.0', - 'depends': ['base'], - 'author': "Author Name", - 'category': 'Category', - 'description': """ - Description text - """, - # data files always loaded at installation - 'data': [ - 'mymodule_view.xml', - ], - # data files containing optionally loaded demonstration data - 'demo': [ - 'demo_data.xml', - ], - } +An Odoo module is declared by its :ref:`manifest <reference/module/manifest>`. +See the :ref:`manifest documentation <reference/module/manifest>` information +about it. A module is also a `Python package <http://docs.python.org/2/tutorial/modules.html#packages>`_ @@ -108,15 +86,15 @@ files in the module. For instance, if the module has a single ``mymodule.py`` file ``__init__.py`` might contain:: - import mymodule + from . import mymodule -Fortunately, there is a mechanism to help you set up an module. The command -``odoo.py`` has a subcommand :ref:`scaffold <reference/cmdline/scaffold>` to -create an empty module: +Odoo provides a mechanism to help set up a new module, :ref:`odoo.py +<reference/cmdline/server>` has a subcommand :ref:`scaffold +<reference/cmdline/scaffold>` to create an empty module: -.. code:: bash +.. code-block:: console - odoo.py scaffold <module name> <where to put it> + $ odoo.py scaffold <module name> <where to put it> The command creates a subdirectory for your module, and automatically creates a bunch of standard files for a module. Most of them simply contain commented code diff --git a/doc/howtos/backend/exercise-creation b/doc/howtos/backend/exercise-creation index 4b780bddbb7..353556a43b5 100644 --- a/doc/howtos/backend/exercise-creation +++ b/doc/howtos/backend/exercise-creation @@ -46,8 +46,8 @@ Index: addons/openacademy/__init__.py +++ addons/openacademy/__init__.py 2014-08-26 17:25:49.791783523 +0200 @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- -+import controllers -+import models ++from . import controllers ++from . import models Index: addons/openacademy/controllers.py =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 diff --git a/doc/howtos/backend/exercise-dashboard b/doc/howtos/backend/exercise-dashboard index 4e3e563f450..bf175edc7d3 100644 --- a/doc/howtos/backend/exercise-dashboard +++ b/doc/howtos/backend/exercise-dashboard @@ -54,6 +54,7 @@ Index: addons/openacademy/views/session_board.xml + <record model="ir.ui.view" id="board_session_form"> + <field name="name">Session Dashboard Form</field> + <field name="model">board.board</field> ++ <field name="type">form</field> + <field name="arch" type="xml"> + <form string="Session Dashboard"> + <board style="2-1"> @@ -89,6 +90,6 @@ Index: addons/openacademy/views/session_board.xml + name="Session Dashboard" parent="base.menu_reporting_dashboard" + action="open_board_session" + sequence="1" -+ id="menu_board_session"/> ++ id="menu_board_session" icon="terp-graph"/> + </data> +</openerp> diff --git a/doc/howtos/backend/exercise-model-inheritance b/doc/howtos/backend/exercise-model-inheritance index ddc84ea6886..a3529403d55 100644 --- a/doc/howtos/backend/exercise-model-inheritance +++ b/doc/howtos/backend/exercise-model-inheritance @@ -7,9 +7,9 @@ Index: addons/openacademy/__init__.py +++ addons/openacademy/__init__.py 2014-08-26 17:26:01.219783354 +0200 @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- - import controllers - import models -+import partner + from . import controllers + from . import models ++from . import partner Index: addons/openacademy/__openerp__.py =================================================================== --- addons.orig/openacademy/__openerp__.py 2014-08-26 17:26:01.227783353 +0200 diff --git a/doc/howtos/backend/exercise-wizard b/doc/howtos/backend/exercise-wizard index 1605185bf05..906cdef181b 100644 --- a/doc/howtos/backend/exercise-wizard +++ b/doc/howtos/backend/exercise-wizard @@ -3,10 +3,10 @@ Index: addons/openacademy/__init__.py --- addons.orig/openacademy/__init__.py 2014-08-27 14:19:23.023111330 +0200 +++ addons/openacademy/__init__.py 2014-08-27 14:22:25.043108628 +0200 @@ -2,3 +2,4 @@ - import controllers - import models - import partner -+import wizard + from . import controllers + from . import models + from . import partner ++from . import wizard Index: addons/openacademy/wizard.py =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst index 2c6eef2d405..dbc59f6392d 100644 --- a/doc/howtos/themes.rst +++ b/doc/howtos/themes.rst @@ -30,7 +30,7 @@ with the structure: |-- pages.xml `-- snippets.xml -``static/styles`` contains your stylesheet(s), ``views`` contains the various +``static/style`` contains your stylesheet(s), ``views`` contains the various XML files describing the theme and theme features to Odoo. Static Page @@ -51,7 +51,12 @@ copy paste the lines:: </t> </template> -Refresh the page and feel the hit. +Comment the lines about ``options.xml`` and ``pages.xml`` in ``__openerp__.py``. + +Run the server and update the theme (``./odoo.py -u dummy_theme``), then +install the theme in the settings menu. + +Go to ``http://localhost:8069/page/hello`` feel the hit. Editing content on a page ''''''''''''''''''''''''' @@ -104,14 +109,25 @@ modify these lines as below: .. code-block:: css - .h1 { + h1 { color: #215487; } - .span { + span { border: 2px solid black; background-color: #eee; } +Compile your file (http://lesscss.org/usage/#command-line-usage) to get the +``custom.css`` file. + +Add this asset to your template in ``page.xml``:: + + <template id="dummy_theme_asset" name="website assete for Dummy theme" inherit_id="website.theme"> + <xpath expr="." position="inside"> + <link rel="stylesheet" href="/dummy_theme/static/style/custom.css"/> + </xpath> + </template> + Refresh the page and feel the hit. Get the most of the dom @@ -267,9 +283,9 @@ Adding images to your snippet You can easely add images in your snippets simply by setting up css backgrounds images. -In ``odoo/addons/theme_dummy/static/`` create a new folder and name it -``img``. Put your images there, in sub-folders if needed. Open -:file:`odoo/addons/theme_dummy/static/style/custom.less`, add these lines +Put your images in ``odoo/addons/theme_dummy/static/img``, or in sub-folders +if needed. Open :file:`odoo/addons/theme_dummy/static/style/custom.less`, add +these lines .. code-block:: css @@ -279,28 +295,20 @@ In ``odoo/addons/theme_dummy/static/`` create a new folder and name it @img-02: url("../img/img-girl.png"); .dummy-girl { background-image: @img-02; } +Add also the ``width`` and the ``height`` css attributes to theses new classes. + In :file:`odoo/addons/theme_dummy/views/pages.xml` change the correspondant lines as below:: - <section> - <div class="container"> - <div class="row dummy-bg"> - <div class="alert alert-primary" role="alert"> - <a href="#" class="alert-link">...</a> - </div> - <div class="col-md-6"> - <h2>BLUE it!</h2> - <div class="dummy-boy"> - </div> - </div> - <div class="col-md-6"> - <h2>GREEN THAT!</h2> - <div class="dummy-girl"> - </div> - </div> - </div> - </div> - </section> + <div class="row"> + <div class="col-md-12 text-center mt32 mb32"> + <h2 class="options_simple_snippet">A great Title, prmize</h2> + <h3 class="text-muted">And a great subtilte too</h3> + <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. </p> + <div class="dummy-boy"></div> + <div class="dummy-girl"></div> + </div> + </div> Your new snippet is now ready to use. Just drag and drop it on your page to see it in action. @@ -329,9 +337,12 @@ Explain xpath .. TODO:: syntax not correct (see website examples) -Your option menu is now correctly set in the database, you can create an new dropdown menu. +Your option menu is now correctly set in the database, you can create an new +dropdown menu. + +Let's say yout want three options which will change the text color and the +background. -Let's say yout want three options which will change the text color and the background. In option.xml, add these lines inside the xpath:: <li data-check_class="text-purple"><a>YOUR OPTION 1</a></li> diff --git a/doc/reference/actions.rst b/doc/reference/actions.rst index 8e31d58b36c..fe2dfb6d68c 100644 --- a/doc/reference/actions.rst +++ b/doc/reference/actions.rst @@ -204,6 +204,7 @@ field: .. code-block:: xml <record model="ir.actions.server" id="print_instance"> + <field name="name">Res Partner Server Action</field> <field name="model_id" ref="model_res_partner"/> <field name="code"> print self, object @@ -218,6 +219,7 @@ field: .. code-block:: xml <record model="ir.actions.server" id="print_instance"> + <field name="name">Res Partner Server Action</field> <field name="model_id" ref="model_res_partner"/> <field name="code"> if object.some_condition(): diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst index 269f62ed2eb..280cd079622 100644 --- a/doc/reference/cmdline.rst +++ b/doc/reference/cmdline.rst @@ -249,6 +249,35 @@ customize the amout of logging output enables DEBUG logging of SQL querying, equivalent to ``--log-handler=openerp.sql_db:DEBUG`` +.. option:: --log-level <level> + + Shortcut to more easily set predefined levels on specific loggers. "real" + levels (``critical``, ``error``, ``warn``, ``debug``) are set on the + ``openerp`` and ``werkzeug`` loggers (except for ``debug`` which is only + set on ``openerp``). + + Odoo also provides debugging pseudo-levels which apply to different sets + of loggers: + + ``debug_sql`` + sets the SQL logger to ``debug`` + + equivalent to ``--log-sql`` + ``debug_rpc`` + sets the ``openerp`` and HTTP request loggers to ``debug`` + + equivalent to ``--log-level debug --log-request`` + ``debug_rpc_answer`` + sets the ``openerp`` and HTTP request and response loggers to + ``debug`` + + equivalent to ``--log-level debug --log-request --log-response`` + + .. note:: + + In case of conflict between :option:`--log-level` and + :option:`--log-handler`, the latter is used + .. _reference/cmdline/scaffold: Scaffolding diff --git a/doc/reference/module.rst b/doc/reference/module.rst index 14e7847f1da..5dd93ca2cf6 100644 --- a/doc/reference/module.rst +++ b/doc/reference/module.rst @@ -15,6 +15,27 @@ and to specify a number of module metadata. It is a file called ``__openerp__.py`` and contains a single Python dictionary, each dictionary key specifying a module metadatum. +:: + + { + 'name': "A Module", + 'version': '1.0', + 'depends': ['base'], + 'author': "Author Name", + 'category': 'Category', + 'description': """ + Description text + """, + # data files always loaded at installation + 'data': [ + 'mymodule_view.xml', + ], + # data files containing optionally loaded demonstration data + 'demo': [ + 'demo_data.xml', + ], + } + Available manifest fields are: ``name`` (``str``, required) diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst index 08170da0d23..a7564ded394 100644 --- a/doc/reference/qweb.rst +++ b/doc/reference/qweb.rst @@ -418,7 +418,7 @@ the name of the template to alter as parameter. The alteration is then performed with any number of ``t-jquery`` sub-directives:: - <t t-extends="base.template"> + <t t-extend="base.template"> <t t-jquery="ul" t-operation="append"> <li>new element</li> </t> diff --git a/doc/reference/testing.rst b/doc/reference/testing.rst index 2676c2b4084..e1c78919f15 100644 --- a/doc/reference/testing.rst +++ b/doc/reference/testing.rst @@ -24,8 +24,16 @@ and ``__init__.py`` contains:: from . import test_foo, test_bar -.. note:: test modules which are not imported from ``tests/__init__.py`` will - not be run +.. warning:: + + test modules which are not imported from ``tests/__init__.py`` will not be + run + +.. versionchanged:: 8.0 + + previously, the test runner would only run modules added to two lists + ``fast_suite`` and ``checks`` in ``tests/__init__.py``. In 8.0 it will + run all imported modules The test runner will simply run any test case, as described in the official `unittest documentation`_, but Odoo provides a number of utilities and helpers @@ -46,8 +54,8 @@ been installed, and not run right after the module installation: .. autofunction:: openerp.tests.common.post_install The most common situation is to use -:class:`~openerp.tests.common.TransactionCase` and test a property of a of a -model in each method:: +:class:`~openerp.tests.common.TransactionCase` and test a property of a model +in each method:: class TestModelA(common.TransactionCase): def test_some_action(self): @@ -67,7 +75,7 @@ Tests are automatically run when installing or updating modules if Odoo server. As of Odoo 8, running tests outside of the install/update cycle is not - +supported. .. _unittest2: http://pypi.python.org/pypi/unittest2 .. _unittest documentation: https://docs.python.org/2/library/unittest.html diff --git a/doc/setup/install.rst b/doc/setup/install.rst index f4758c1281f..7fb83ea4667 100644 --- a/doc/setup/install.rst +++ b/doc/setup/install.rst @@ -33,21 +33,19 @@ This documents attempts to describe most of the installation options. Good for developing modules, can be used as base for production deployment. -:ref:`setup/install/vcs` - Mostly has the same strengths and weaknesses as the - :ref:`setup/install/source`, but allows (technically) simpler - switching between versions of Odoo, as well as more structured patching - and customisations of Odoo itself (not through custom modules): with a - standard :ref:`setup/install/source`, a custom solution has to be - implemented to keep track of patches applied to Odoo (e.g. Quilt_). With - a VCS checkout, these changes can be implemented as standard Git revisions - or a custom Git branch, and can more easily be contributed back to the - main project. + + The source code can be obtained by downloading a tarball or using git. + Using git makes it easier to update, switch between multiple versions + (including the current development version) or contribute. +`docker image <https://registry.hub.docker.com/_/odoo/>`_ + if you usually use docker_ for development or deployment, an official + docker_ base image is available, see the image's help document for more + information .. _setup/install/demo: -Demo_ -===== +Demo +==== To simply get a quick idea of Odoo, demo_ instances are available. They are shared instances which only live for a few hours, and can be used to browse @@ -57,8 +55,8 @@ Demo_ instances require no local installation, just a web browser. .. _setup/install/saas: -SaaS_ -===== +SaaS +==== Odoo's SaaS_ provides private instances and starts out free. It can be used to discover and test Odoo and do non-code customizations without having to @@ -72,11 +70,8 @@ browser is sufficient. Packaged installers =================== -.. Odoo provides packaged installers for Windows, deb-based distributions -.. (Debian, Ubuntu, …) and RPM-based distributions (Fedora, CentOS, RHEL, …). - -Odoo provides packaged installers for Windows and deb-based distributions -(Debian, Ubuntu, …). +Odoo provides packaged installers for Windows, deb-based distributions +(Debian, Ubuntu, …) and RPM-based distributions (Fedora, CentOS, RHEL, …). These packages automatically set up all dependencies, but may be difficult to keep up-to-date. @@ -84,6 +79,33 @@ keep up-to-date. Official packages with all relevant dependency requirements are available on https://nightly.odoo.com. +Windows +------- + +* download https://nightly.odoo.com/8.0/nightly/exe/odoo_8.0.latest.exe +* run the downloaded file + + .. warning:: on Windows 8, you may see a warning titled "Windows protected + your PC". Click :guilabel:`More Info` then + :guilabel:`Run anyway` + +* Accept the UAC_ prompt +* Go through the various installation steps + +Odoo will automatically be started at the end of the installation. + +Configuration +''''''''''''' + +The :ref:`configuration file <reference/cmdline/config>` can be found at +:file:`{%PROGRAMFILES%}\\Odoo 8.0-{id}\\server\\openerp-server.conf`. + +The configuration file can be edited to connect to a remote Postgresql, edit +file locations or set a dbfilter. + +To reload the configuration file, restart the Odoo service via +:menuselection:`Services --> odoo server`. + Deb --- @@ -99,10 +121,6 @@ commands as root: This will automatically install all dependencies, install Odoo itself as a daemon and automatically start it. -.. postgres-server apparently automatically installed due to being a - recommended package, maybe install it explicitly for the purpose of this - document? - .. danger:: to print PDF reports, you must install wkhtmltopdf_ yourself: the version of wkhtmltopdf_ available in debian repositories does not support headers and footers so it can not be installed @@ -167,32 +185,6 @@ When the configuration file is edited, Odoo must be restarted via SystemD: $ sudo systemctl restart odoo -Windows -------- - -* download https://nightly.odoo.com/8.0/nightly/exe/odoo_8.0.latest.exe -* run the downloaded file - - .. warning:: on Windows 8, you may see a warning titled "Windows protected - your PC". Click :guilabel:`More Info` then - :guilabel:`Run anyway` - -* Accept the UAC_ prompt -* Go through the various installation steps - -Odoo will automatically be started at the end of the installation. - -Configuration -''''''''''''' - -The :ref:`configuration file <reference/cmdline/config>` can be found at -:file:`{%PROGRAMFILES%}\\Odoo 8.0-{id}\\server\\openerp-server.conf`. - -The configuration file can be edited to connect to a remote Postgresql, edit -file locations or set a dbfilter. - -To reload the configuration file, restart the Odoo service via -:menuselection:`Services --> odoo server`. .. _setup/install/source: @@ -214,77 +206,168 @@ edit a configuration file. Finally it provides greater control over the system's set up, and allows more easily keeping (and running) multiple versions of Odoo side-by-side. -The Odoo source can be downloaded from -https://nightly.odoo.com/8.0/nightly/src/odoo_8.0-latest.tar.gz +There are two way to get the odoo source source zip or git. + +* Odoo zip can be downloaded from + https://nightly.odoo.com/8.0/nightly/src/odoo_8.0.latest.zip, the zip file + then needs to be uncompressed to use its content + +* git allows simpler update and easier switching between differents versions + of Odoo. It also simplifies maintaining non-module patches and + contributions. The primary drawback of git is that it is significantly + larger than a tarball as it contains the entire history of the Odoo project. -.. warning:: Windows does not handle .tar.gz archives natively, you will have - to download and install `7-Zip <http://www.7-zip.org>`_ to - decompress the archive + The git repository is https://github.com/odoo/odoo.git. + + Downloading it requires a `a git client <http://git-scm.com/download/>`_ + (which may be available via your distribution on linux) and can be performed + using the following command: + + .. code-block:: console + + $ git clone https://github.com/odoo/odoo.git Installing dependencies ----------------------- -Source installation requires manually installing dependencies, be them native -dependencies or Python libraries: +Source installation requires manually installing dependencies: + +* Python 2.7. + + - on Linux and OS X, included by default + - on Windows, use `the official Python 2.7.9 installer + <https://www.python.org/downloads/windows/>`_. + + .. warning:: select "add python.exe to Path" during installation, and + reboot afterwards to ensure the :envvar:`PATH` is updated -* Python, should be preinstalled on most systems. On Windows, use `the - official Python 2.7 installer <https://www.python.org/downloads/windows/>`_. + .. note:: if Python is already installed, make sure it is 2.7.9, previous + versions are less convenient and 3.x versions are not compatible + with Odoo -* PostgreSQL, if you want the database to be on the same machine as Odoo - (simplest and default) +* PostgreSQL, to use a local database + + After installation you will need to create a postgres user: by default the + only user is ``postgres``, and Odoo forbids connecting as ``postgres``. + + - on Linux, use your distribution's package, then create a postgres user + named like your login: + + .. code-block:: console + + $ sudo su - postgres -c "createuser -s $USER" + + Because the role login is the same as your unix login unix sockets can be + use without a password. - - on Linux, use your distribution's package - - on Windows, use `the official installer`_ - on OS X, `postgres.app <http://postgresapp.com>`_ is the simplest way to - get started + get started, then create a postgres user as on Linux + + - on Windows, use `PostgreSQL for windows`_ then - .. todo:: create new role? + - add PostgreSQL's ``bin`` directory (default: + ``C:\Program Files\PostgreSQL\9.4\bin``) to your :envvar:`PATH` + - create a postgres user with a password using the pg admin gui: open + pgAdminIII, double-click the server to create a connection, select + :menuselection:`Edit --> New Object --> New Login Role`, enter the + usename in the :guilabel:`Role Name` field (e.g. ``odoo``), then open + the :guilabel:`Definition` tab and enter the password (e.g. ``odoo``), + then click :guilabel:`OK`. + + The user and password must be passed to Odoo using either the + :option:`-w <odoo.py -w>` and :option:`-r <odoo.py -r>` options or + :ref:`the configuration file <reference/cmdline/config>` + +* Python dependencies listed in the :file:`requirements.txt` file. + + - on Linux, python dependencies may be installable with the system's package + manager or using pip. + + For libraries using native code (Pillow, lxml, greenlet, gevent, psycopg2, + ldap) it may be necessary to install development tools and native + dependencies before pip is able to install the dependencies themselves. + These are available in ``-dev`` or ``-devel`` packages for Python, + Postgres, libxml2, libxslt, libevent and libsasl2. Then the Python + dependecies can themselves be installed: + + .. code-block:: console -* the :file:`requirements.txt` file in the source lists all the Python - dependencies + $ pip install -r requirements.txt - - for Windows, http://www.lfd.uci.edu/~gohlke/pythonlibs/ provides a number - of libraries, for both pure-python and native, packaged as installers - - dependencies may be installable with the system's package manager - - pip_ can take the requirements file directly and install everything listed - in it, either globally or within a `virtual environment`_: + - on OS X, you will need to install the Command Line Tools + (``xcode-select --install``) then download and install a package manager + of your choice (homebrew_, macports_) to install non-Python dependencies. + pip can then be used to install the Python dependencies as on Linux: .. code-block:: console - $ pip install -r path/to/requirements.txt + $ pip install -r requirements.txt - For libraries using native code (Pillow, lxml, greenlet, gevent, psycopg2) - it may be necessary to install development tools and native dependencies - before pip is able to install the dependencies themselves: + - on Windows you need to install some of the dependencies manually, tweak the + requirements.txt file, then run pip to install the remaning ones. - * Linux distributions generally require ``-dev`` or ``-devel`` packages - for Python, Postgres, libxml2, libxslt and libevent - * for OSX, install the Command Line Tools (``xcode-select --install``) the - native dependency via your preferred package manager (macports_, - homebrew_) - * for Windows + Install ``psycopg`` using the installer here + http://www.stickpeople.com/projects/python/win-psycopg/ - .. danger:: you will have to remove ``python-ldap`` from the - requirements file, it can not be installed via pip_ on - Windows and must be installed as a precompiled binary + Then edit the requirements.txt file: - - install the `Visual C++ Compiler for Python 2.7`_ (and restart) - - install `PostgreSQL for windows`_ if not already done - - Add PostgreSQL's ``bin`` directory (default: - ``C:\Program Files\PostgreSQL\9.3\bin``) to your :envvar:`PATH` + - remove ``psycopg2`` as you already have it. + - remove the optional ``python-ldap``, ``gevent`` and ``psutil`` because + they require compilation. + - add ``pypiwin32`` because it's needed under windows. - ``greenlet``, ``Pillow`` and ``lxml`` are available as pre-compiled - wheels_ and trivially installed by pip_, ``gevent`` only needs the - compiler to be installable and a local installation ``psycopg2`` + Then use pip to install install the dependencies using the following + command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual + path where you downloaded Odoo): - .. note:: by default, Python scripts are not on the PATH in windows, - after installing pip_ add ``C:\Python27\Scripts`` to your - :envvar:`PATH` + .. code-block:: ps1 -.. danger:: whatever the installation method, Odoo on Windows also needs - pywin32_ which is not listed in the requirements file. It can be - installed using pip_ with ``pip install pypiwin32``, or manually - by downloading the official pywin32_ installer and running it. + C:\> cd \YourOdooPath + C:\YourOdooPath> C:\Python27\Scripts\pip.exe install -r requirements.txt + +* *Less CSS* via nodejs + + - on Linux, use your distribution's package manager to install nodejs and + npm. + + .. warning:: + + In debian wheezy and Ubuntu 13.10 and before you need to install + nodejs manually: + + .. code-block:: console + + $ wget -qO- https://deb.nodesource.com/setup | bash - + $ apt-get install -y nodejs + + In later debian (>jessie) and ubuntu (>14.04) you may need to add a + symlink as npm packages call ``node`` but debian calls the binary + ``nodejs`` + + .. code-block:: console + + $ apt-get install -y npm + $ sudo ln -s /usr/bin/nodejs /usr/bin/node + + Once npm is installed, use it to install less and less-plugin-clean-css: + + .. code-block:: console + + $ sudo npm install -g less less-plugin-clean-css + + - on OS X, install nodejs via your preferred package manager (homebrew_, + macports_) then install less and less-plugin-clean-css: + + .. code-block:: console + + $ sudo npm install -g less less-plugin-clean-css + + - on Windows, `install nodejs <http://nodejs.org/download/>`_, reboot (to + update the :envvar:`PATH`) and install less and less-plugin-clean-css: + + .. code-block:: ps1 + + C:\> npm install -g less less-plugin-clean-css Running Odoo ------------ @@ -297,33 +380,37 @@ Once all dependencies are set up, Odoo can be launched by running ``odoo.py``. Common necessary configurations are: -* PostgreSQL host, port, user and password. Odoo has no defaults beyond +* PostgreSQL host, port, user and password. + + Odoo has no defaults beyond `psycopg2's defaults <http://initd.org/psycopg/docs/module.html>`_: connects - over a UNIX socket on port 5432 with the current user and no password. + over a UNIX socket on port 5432 with the current user and no password. By + default this should work on Linux and OS X, but it *will not work* on + windows as it does not support UNIX sockets. - This may require creating new PostgreSQL roles, by default the only user is - ``postgres``, and Odoo forbids connecting as ``postgres``. * Custom addons path beyond the defaults, to load your own modules -.. _setup/install/vcs: +Under Windows a typical way to execute odoo would be: + +.. code-block:: ps1 -VCS Checkout -============ + C:\YourOdooPath> python odoo.py -w odoo -r odoo --addons-path=addons,../mymodules --db-filter=mydb$ -The VCS Checkout installation method is similar to -:ref:`source-based installation <setup/install/source>` in most respect. +Where ``odoo``, ``odoo`` are the postgresql login and password, +``../mymodules`` a directory with additional addons and ``mydb`` the default +db to serve on localhost:8069 + +Under Unix a typical way to execute odoo would be: + +.. code-block:: console -* Instead of downloading a tarball the Odoo source code is downloaded from - `the repository`_ using git_ -* This simplifies the development and contributions to Odoo itself -* This also simplifies maintaining non-module patches on top of the base Odoo - system + $ ./odoo.py --addons-path=addons,../mymodules --db-filter=mydb$ -The primary drawback of the VCS checkout method is that it is significantly -larger than a :ref:`source install <setup/install/source>` as it contains -the entire history of the Odoo project. +Where ``../mymodules`` is a directory with additional addons and ``mydb`` the +default db to serve on localhost:8069 .. _demo: https://demo.odoo.com +.. _docker: https://www.docker.com .. _EPEL: https://fedoraproject.org/wiki/EPEL .. _PostgreSQL: http://www.postgresql.org .. _the official installer: diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml index 4474ade928d..b67ee1ed72b 100644 --- a/openerp/addons/base/base_data.xml +++ b/openerp/addons/base/base_data.xml @@ -88,7 +88,7 @@ Administrator</span>]]></field> <record id="group_portal" model="res.groups"> <field name="name">Portal</field> <field name="comment">Portal members have specific access rights (such as record rules and restricted menus). - They usually do not belong to the usual OpenERP groups.</field> + They usually do not belong to the usual Odoo groups.</field> </record> <!-- A group dedicated to the public user only, making groups @@ -97,7 +97,7 @@ Administrator</span>]]></field> <record id="group_public" model="res.groups"> <field name="name">Public</field> <field name="comment">Public users have specific access rights (such as record rules and restricted menus). - They usually do not belong to the usual OpenERP groups.</field> + They usually do not belong to the usual Odoo groups.</field> </record> <!-- Basic fonts family included in PDF standart, will always be in the font list --> diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index c01c42f6c08..bca23a9bbcd 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -329,7 +329,6 @@ class ir_actions_act_window(osv.osv): 'auto_search':True, 'multi': False, } - def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): """ call the method get_empty_list_help of the model and set the window action help message """ @@ -338,24 +337,22 @@ class ir_actions_act_window(osv.osv): ids = [ids] results = super(ir_actions_act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load) - context = dict(context or {}) - eval_dict = { - 'active_model': context.get('active_model'), - 'active_id': context.get('active_id'), - 'active_ids': context.get('active_ids'), - 'uid': uid, - 'context': context, - } - for res in results: - model = res.get('res_model') - if model in self.pool: - try: - with tools.mute_logger("openerp.tools.safe_eval"): - eval_context = eval(res['context'] or "{}", eval_dict) or {} - res['context'] = str(eval_context) - except Exception: - continue - if not fields or 'help' in fields: + if not fields or 'help' in fields: + context = dict(context or {}) + eval_dict = { + 'active_model': context.get('active_model'), + 'active_id': context.get('active_id'), + 'active_ids': context.get('active_ids'), + 'uid': uid, + } + for res in results: + model = res.get('res_model') + if model and self.pool.get(model): + try: + with tools.mute_logger("openerp.tools.safe_eval"): + eval_context = eval(res['context'] or "{}", eval_dict) or {} + except Exception: + continue custom_context = dict(context, **eval_context) res['help'] = self.pool[model].get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context) if ids_int: diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 2a8df6c2fd2..79de91e2ebb 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -90,7 +90,7 @@ class ir_http(osv.AbstractModel): # let them bubble up request.session.logout(keep_db=True) getattr(self, "_auth_method_%s" % auth_method)() - except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException): + except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException, werkzeug.exceptions.HTTPException): raise except Exception: _logger.info("Exception during request Authentication.", exc_info=True) diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index f23f2342387..2e47c8f5edf 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -20,7 +20,6 @@ ############################################################################## import logging -import unicodedata from openerp import tools import openerp.modules @@ -358,8 +357,7 @@ class ir_translation(osv.osv): trad = res and res[0] or u'' if source and not trad: return tools.ustr(source) - # Remove control characters - return filter(lambda c: unicodedata.category(c) != 'Cc', tools.ustr(trad)) + return trad def create(self, cr, uid, vals, context=None): if context is None: diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 94406195d07..054ecf9d045 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -761,10 +761,17 @@ class view(osv.osv): if node.get('string') and node.get('string').strip() and not result: term = node.get('string').strip() trans = Translations._get_source(cr, user, model, 'view', context['lang'], term) - if trans == term and ('base_model_name' in context): - # If translation is same as source, perhaps we'd have more luck with the alternative model name - # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model - trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], term) + if trans == term: + if 'base_model_name' in context: + # If translation is same as source, perhaps we'd have more luck with the alternative model name + # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model + trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], term) + else: + inherit_model = self.browse(cr, user, view_id, context=context).inherit_id.model or model + if inherit_model != model: + # parent view has a different model, if the terms belongs to the parent view, the translation + # should be checked on the parent model as well + trans = Translations._get_source(cr, user, inherit_model, 'view', context['lang'], term) if trans: node.set('string', trans) diff --git a/openerp/addons/base/module/wizard/base_export_language_view.xml b/openerp/addons/base/module/wizard/base_export_language_view.xml index 882eb6a68bc..6de413e01e0 100644 --- a/openerp/addons/base/module/wizard/base_export_language_view.xml +++ b/openerp/addons/base/module/wizard/base_export_language_view.xml @@ -25,11 +25,11 @@ <li>PO(T) format: you should edit it with a PO editor such as <a href="http://www.poedit.net/" target="_blank">POEdit</a>, or your preferred text editor</li> <li>TGZ format: this is a compressed archive containing a PO file, directly suitable - for uploading to OpenERP's translation platform, + for uploading to Odoo's translation platform, <a href="https://translations.launchpad.net/openobject-addons" target="_blank">Launchpad</a></li> </ul> </p> - <p>For more details about translating OpenERP in your language, please refer to the + <p>For more details about translating Odoo in your language, please refer to the <a href="http://doc.openerp.com/v6.1/contribute/07_improving_translations.html" target="_blank">documentation</a>.</p> </div> <footer states="choose"> diff --git a/openerp/addons/base/res/res_lang.py b/openerp/addons/base/res/res_lang.py index 2c39516c66f..bcf8f4c68de 100644 --- a/openerp/addons/base/res/res_lang.py +++ b/openerp/addons/base/res/res_lang.py @@ -128,6 +128,15 @@ class lang(osv.osv): return False return True + def _check_grouping(self, cr, uid, ids, context=None): + for lang in self.browse(cr, uid, ids, context=context): + try: + if not all(isinstance(x, int) for x in eval(lang.grouping)): + return False + except Exception: + return False + return True + def _get_default_date_format(self, cursor, user, context=None): return '%m/%d/%Y' @@ -163,7 +172,8 @@ class lang(osv.osv): ] _constraints = [ - (_check_format, 'Invalid date/time format directive specified. Please refer to the list of allowed directives, displayed when you edit a language.', ['time_format', 'date_format']) + (_check_format, 'Invalid date/time format directive specified. Please refer to the list of allowed directives, displayed when you edit a language.', ['time_format', 'date_format']), + (_check_grouping, "The Separator Format should be like [,n] where 0 < n :starting from Unit digit.-1 will end the separation. e.g. [3,2,-1] will represent 106500 to be 1,06,500;[1,2,-1] will represent it to be 106,50,0;[3] will represent it as 106,500. Provided ',' as the thousand separator in each case.", ['grouping']) ] @tools.ormcache(skiparg=3) diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 65c88fc64d0..704bfa85161 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -215,7 +215,7 @@ <div class="oe_module_vignette"> <a type="open" class="col-md-2"> <t t-if="record.has_image.raw_value === true"> - <img t-att-src="kanban_image('res.partner', 'image', record.id.value, {'preview_image': 'image_small'})" class="oe_avatar oe_kanban_avatar_smallbox"/> + <img t-att-src="kanban_image('res.partner', 'image_small', record.id.value)" class="oe_avatar oe_kanban_avatar_smallbox"/> </t> <t t-if="record.image and record.image.raw_value !== false"> <img t-att-src="'data:image/png;base64,'+record.image.raw_value" class="oe_avatar oe_kanban_avatar_smallbox"/> diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index c9f8fca2e52..95a6b8ce653 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -732,6 +732,10 @@ class groups_view(osv.osv): # and introduces the reified group fields # we have to try-catch this, because at first init the view does not exist # but we are already creating some basic groups + if not context or context.get('install_mode'): + # use installation/admin language for translatable names in the view + context = dict(context or {}) + context.update(self.pool['res.users'].context_get(cr, uid)) view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context) if view and view.exists() and view._name == 'ir.ui.view': xml1, xml2 = [], [] diff --git a/openerp/addons/test_convert/tests/__init__.py b/openerp/addons/test_convert/tests/__init__.py index 002a0a18d1a..2b608870b30 100644 --- a/openerp/addons/test_convert/tests/__init__.py +++ b/openerp/addons/test_convert/tests/__init__.py @@ -1,6 +1,2 @@ # -*- coding: utf-8 -*- from . import test_convert - -checks = [ - test_convert -] diff --git a/openerp/addons/test_converter/tests/__init__.py b/openerp/addons/test_converter/tests/__init__.py index a87d81e524f..196a4ec672c 100644 --- a/openerp/addons/test_converter/tests/__init__.py +++ b/openerp/addons/test_converter/tests/__init__.py @@ -2,10 +2,3 @@ from . import test_html, test_gbf -fast_suite = [ -] - -checks = [ - test_html, - test_gbf, -] diff --git a/openerp/addons/test_documentation_examples/tests/__init__.py b/openerp/addons/test_documentation_examples/tests/__init__.py index b9b095e284d..b484ae38a35 100644 --- a/openerp/addons/test_documentation_examples/tests/__init__.py +++ b/openerp/addons/test_documentation_examples/tests/__init__.py @@ -1,12 +1,4 @@ # -*- coding: utf-8 -*- -from . import test_inheritance, test_extension, test_delegation - -fast_suite = [ -] -checks = [ - test_inheritance, - test_extension, - test_delegation, -] +from . import test_inheritance, test_extension, test_delegation diff --git a/openerp/addons/test_impex/tests/__init__.py b/openerp/addons/test_impex/tests/__init__.py index aff1cb0ad95..031da993781 100644 --- a/openerp/addons/test_impex/tests/__init__.py +++ b/openerp/addons/test_impex/tests/__init__.py @@ -2,11 +2,3 @@ from . import test_export, test_import, test_load -fast_suite = [ -] - -checks = [ - test_export, - test_import, - test_load, -] diff --git a/openerp/addons/test_inherit/tests/__init__.py b/openerp/addons/test_inherit/tests/__init__.py index 65bdf71e0c5..ecb37f4bc9a 100644 --- a/openerp/addons/test_inherit/tests/__init__.py +++ b/openerp/addons/test_inherit/tests/__init__.py @@ -2,9 +2,3 @@ from . import test_inherit -fast_suite = [ -] - -checks = [ - test_inherit, -] diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py index 6a2233427f6..9866c8b6fd4 100644 --- a/openerp/addons/test_new_api/models.py +++ b/openerp/addons/test_new_api/models.py @@ -106,7 +106,7 @@ class Message(models.Model): display_name = fields.Char(string='Abstract', compute='_compute_display_name') size = fields.Integer(compute='_compute_size', search='_search_size') double_size = fields.Integer(compute='_compute_double_size') - discussion_name = fields.Char(related='discussion.name', readonly=True) + discussion_name = fields.Char(related='discussion.name') @api.one @api.constrains('author', 'discussion') diff --git a/openerp/addons/test_new_api/tests/__init__.py b/openerp/addons/test_new_api/tests/__init__.py index dd9a2d44ec2..790ad9c9a59 100644 --- a/openerp/addons/test_new_api/tests/__init__.py +++ b/openerp/addons/test_new_api/tests/__init__.py @@ -5,14 +5,3 @@ from . import test_new_fields from . import test_onchange from . import test_field_conversions from . import test_attributes - -fast_suite = [ -] - -checks = [ - test_related, - test_new_fields, - test_onchange, - test_field_conversions, - test_attributes, -] diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py index 46e9e512a7f..5441698e7ed 100644 --- a/openerp/addons/test_new_api/tests/test_new_fields.py +++ b/openerp/addons/test_new_api/tests/test_new_fields.py @@ -292,7 +292,7 @@ class TestNewFields(common.TransactionCase): # by default related fields are not stored field = message._fields['discussion_name'] self.assertFalse(field.store) - self.assertTrue(field.readonly) + self.assertFalse(field.readonly) # check value of related field self.assertEqual(message.discussion_name, discussion.name) diff --git a/openerp/addons/test_workflow/tests/__init__.py b/openerp/addons/test_workflow/tests/__init__.py index 9bb88689e8a..2aff5c6b071 100644 --- a/openerp/addons/test_workflow/tests/__init__.py +++ b/openerp/addons/test_workflow/tests/__init__.py @@ -2,9 +2,3 @@ from . import test_workflow -fast_suite = [ -] - -checks = [ - test_workflow, -] diff --git a/openerp/api.py b/openerp/api.py index 973e78863fc..6f675afd89f 100644 --- a/openerp/api.py +++ b/openerp/api.py @@ -73,7 +73,6 @@ from openerp.tools import frozendict _logger = logging.getLogger(__name__) # The following attributes are used, and reflected on wrapping methods: -# - method._api: decorator function, used for re-applying decorator # - method._constrains: set by @constrains, specifies constraint dependencies # - method._depends: set by @depends, specifies compute dependencies # - method._returns: set by @returns, specifies return model @@ -81,10 +80,11 @@ _logger = logging.getLogger(__name__) # - method.clear_cache: set by @ormcache, used to clear the cache # # On wrapping method only: +# - method._api: decorator function, used for re-applying decorator # - method._orig: original method # -WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_api', '_constrains', +WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_constrains', '_depends', '_onchange', '_returns', 'clear_cache') INHERITED_ATTRS = ('_returns',) @@ -121,10 +121,14 @@ identity = lambda x: x def decorate(method, attr, value): """ Decorate `method` or its original method. """ - # decorate the original method, and re-apply the api decorator, if any - orig = getattr(method, '_orig', method) - setattr(orig, attr, value) - return getattr(method, '_api', identity)(orig) + if getattr(method, '_api', False): + # decorate the original method, and re-apply the api decorator + setattr(method._orig, attr, value) + return method._api(method._orig) + else: + # simply decorate the method itself + setattr(method, attr, value) + return method def propagate(from_method, to_method): """ Propagate decorators from `from_method` to `to_method`, and return the @@ -227,7 +231,7 @@ def returns(model, downgrade=None): return lambda method: decorate(method, '_returns', (model, downgrade)) -def make_wrapper(method, old_api, new_api): +def make_wrapper(decorator, method, old_api, new_api): """ Return a wrapper method for `method`. """ def wrapper(self, *args, **kwargs): # avoid hasattr(self, '_ids') because __getattr__() is overridden @@ -240,6 +244,7 @@ def make_wrapper(method, old_api, new_api): for attr in WRAPPED_ATTRS: if hasattr(method, attr): setattr(wrapper, attr, getattr(method, attr)) + wrapper._api = decorator wrapper._orig = method return wrapper @@ -322,7 +327,6 @@ def model(method): Notice that no `ids` are passed to the method in the traditional style. """ - method._api = model split = get_context_split(method) downgrade = get_downgrade(method) @@ -332,7 +336,7 @@ def model(method): result = method(recs, *args, **kwargs) return downgrade(result) - return make_wrapper(method, old_api, method) + return make_wrapper(model, method, old_api, method) def multi(method): @@ -350,7 +354,6 @@ def multi(method): model.method(cr, uid, ids, args, context=context) """ - method._api = multi split = get_context_split(method) downgrade = get_downgrade(method) @@ -360,7 +363,7 @@ def multi(method): result = method(recs, *args, **kwargs) return downgrade(result) - return make_wrapper(method, old_api, method) + return make_wrapper(multi, method, old_api, method) def one(method): @@ -380,7 +383,6 @@ def one(method): names = model.method(cr, uid, ids, args, context=context) """ - method._api = one split = get_context_split(method) downgrade = get_downgrade(method) aggregate = get_aggregate(method) @@ -395,7 +397,7 @@ def one(method): result = [method(rec, *args, **kwargs) for rec in self] return aggregate(self, result) - return make_wrapper(method, old_api, new_api) + return make_wrapper(one, method, old_api, new_api) def cr(method): @@ -407,7 +409,6 @@ def cr(method): model.method(cr, args) """ - method._api = cr upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -415,12 +416,11 @@ def cr(method): result = method(self._model, cr, *args, **kwargs) return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr, method, method, new_api) def cr_context(method): """ Decorate a traditional-style method that takes `cr`, `context` as parameters. """ - method._api = cr_context upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -429,12 +429,11 @@ def cr_context(method): result = method(self._model, cr, *args, **kwargs) return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_context, method, method, new_api) def cr_uid(method): """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """ - method._api = cr_uid upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -442,7 +441,7 @@ def cr_uid(method): result = method(self._model, cr, uid, *args, **kwargs) return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_uid, method, method, new_api) def cr_uid_context(method): @@ -455,7 +454,6 @@ def cr_uid_context(method): model.method(cr, uid, args, context=context) """ - method._api = cr_uid_context upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -464,7 +462,7 @@ def cr_uid_context(method): result = method(self._model, cr, uid, *args, **kwargs) return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_uid_context, method, method, new_api) def cr_uid_id(method): @@ -472,7 +470,6 @@ def cr_uid_id(method): parameters. Such a method may be called in both record and traditional styles. In the record style, the method automatically loops on records. """ - method._api = cr_uid_id upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -480,7 +477,7 @@ def cr_uid_id(method): result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids] return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_uid_id, method, method, new_api) def cr_uid_id_context(method): @@ -498,7 +495,6 @@ def cr_uid_id_context(method): model.method(cr, uid, id, args, context=context) """ - method._api = cr_uid_id_context upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -507,7 +503,7 @@ def cr_uid_id_context(method): result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids] return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_uid_id_context, method, method, new_api) def cr_uid_ids(method): @@ -515,7 +511,6 @@ def cr_uid_ids(method): parameters. Such a method may be called in both record and traditional styles. """ - method._api = cr_uid_ids upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -523,7 +518,7 @@ def cr_uid_ids(method): result = method(self._model, cr, uid, self.ids, *args, **kwargs) return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_uid_ids, method, method, new_api) def cr_uid_ids_context(method): @@ -543,7 +538,6 @@ def cr_uid_ids_context(method): It is generally not necessary, see :func:`guess`. """ - method._api = cr_uid_ids_context upgrade = get_upgrade(method) def new_api(self, *args, **kwargs): @@ -552,7 +546,7 @@ def cr_uid_ids_context(method): result = method(self._model, cr, uid, self.ids, *args, **kwargs) return upgrade(self, result) - return make_wrapper(method, method, new_api) + return make_wrapper(cr_uid_ids_context, method, method, new_api) def v7(method_v7): @@ -575,7 +569,7 @@ def v7(method_v7): method = frame.f_locals.get(method_v7.__name__) method_v8 = getattr(method, '_v8', method) - wrapper = make_wrapper(method_v7, method_v7, method_v8) + wrapper = make_wrapper(v7, method_v7, method_v7, method_v8) wrapper._v7 = method_v7 wrapper._v8 = method_v8 return wrapper @@ -601,7 +595,7 @@ def v8(method_v8): method = frame.f_locals.get(method_v8.__name__) method_v7 = getattr(method, '_v7', method) - wrapper = make_wrapper(method_v8, method_v7, method_v8) + wrapper = make_wrapper(v8, method_v8, method_v7, method_v8) wrapper._v7 = method_v7 wrapper._v8 = method_v8 return wrapper @@ -664,7 +658,7 @@ def guess(method): def expected(decorator, func): """ Decorate `func` with `decorator` if `func` is not wrapped yet. """ - return decorator(func) if not hasattr(func, '_orig') else func + return decorator(func) if not hasattr(func, '_api') else func diff --git a/openerp/cli/scaffold.py b/openerp/cli/scaffold.py index a4b65690382..8194aa6cbba 100644 --- a/openerp/cli/scaffold.py +++ b/openerp/cli/scaffold.py @@ -83,18 +83,21 @@ env.filters['snake'] = snake env.filters['pascal'] = pascal class template(object): def __init__(self, identifier): - # TODO: directories, archives (zipfile, tarfile) + # TODO: archives (zipfile, tarfile) self.id = identifier - if not os.path.isdir(self.path): - die("{} is not a valid module template".format(identifier)) + # is identifier a builtin? + self.path = builtins(identifier) + if os.path.isdir(self.path): + return + # is identifier a directory? + self.path = identifier + if os.path.isdir(self.path): + return + die("{} is not a valid module template".format(identifier)) def __str__(self): return self.id - @property - def path(self): - return builtins(self.id) - def files(self): """ Lists the (local) path and content of all files in the template """ @@ -118,7 +121,7 @@ class template(object): os.makedirs(destdir) with open(dest, 'wb') as f: - if ext not in ('.py', '.xml', '.csv', '.js'): + if ext not in ('.py', '.xml', '.csv', '.js', '.rst', '.html'): f.write(content) else: env.from_string(content)\ diff --git a/openerp/fields.py b/openerp/fields.py index b16151176a6..030872c0092 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -460,7 +460,8 @@ class Field(object): # determine dependencies, compute, inverse, and search self.depends = ('.'.join(self.related),) self.compute = self._compute_related - self.inverse = self._inverse_related + if not (self.readonly or field.readonly): + self.inverse = self._inverse_related if field._description_searchable: # allow searching on self only if the related field is searchable self.search = self._search_related @@ -1751,6 +1752,14 @@ class Many2many(_RelationalMulti): _column_limit = property(attrgetter('limit')) +class Serialized(Field): + """ Minimal support for existing sparse and serialized fields. """ + type = 'serialized' + + def convert_to_cache(self, value, record, validate=True): + return value or {} + + class Id(Field): """ Special case for field 'id'. """ store = True @@ -1774,7 +1783,6 @@ class Id(Field): def __set__(self, record, value): raise TypeError("field 'id' cannot be assigned") - # imported here to avoid dependency cycle issues from openerp import SUPERUSER_ID from .exceptions import Warning, AccessError, MissingError diff --git a/openerp/http.py b/openerp/http.py index 091353236bd..e3c4039c1c8 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -13,6 +13,7 @@ import inspect import logging import mimetypes import os +import pprint import random import re import sys @@ -25,7 +26,6 @@ import warnings from zlib import adler32 import babel.core -import psutil import psycopg2 import simplejson import werkzeug.contrib.sessions @@ -37,6 +37,11 @@ import werkzeug.wrappers import werkzeug.wsgi from werkzeug.wsgi import wrap_file +try: + import psutil +except ImportError: + psutil = None + import openerp from openerp import SUPERUSER_ID from openerp.service import security, model as service_model @@ -44,6 +49,8 @@ from openerp.tools.func import lazy_property from openerp.tools import ustr _logger = logging.getLogger(__name__) +rpc_request = logging.getLogger(__name__ + '.rpc.request') +rpc_response = logging.getLogger(__name__ + '.rpc.response') # 1 week cache for statics as advised by Google Page Speed STATIC_CACHE = 60 * 60 * 24 * 7 @@ -84,14 +91,13 @@ def dispatch_rpc(service_name, method, params): in a upper layer. """ try: - rpc_request = logging.getLogger(__name__ + '.rpc.request') - rpc_response = logging.getLogger(__name__ + '.rpc.response') rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG) rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG) if rpc_request_flag or rpc_response_flag: start_time = time.time() start_rss, start_vms = 0, 0 - start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() + if psutil: + start_rss, start_vms = psutil.Process(os.getpid()).get_memory_info() if rpc_request and rpc_response_flag: openerp.netsvc.log(rpc_request, logging.DEBUG, '%s.%s' % (service_name, method), replace_request_password(params)) @@ -112,7 +118,8 @@ def dispatch_rpc(service_name, method, params): if rpc_request_flag or rpc_response_flag: end_time = time.time() end_rss, end_vms = 0, 0 - end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() + if psutil: + end_rss, end_vms = psutil.Process(os.getpid()).get_memory_info() logline = '%s.%s time:%.3fs mem: %sk -> %sk (diff: %sk)' % (service_name, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) if rpc_response_flag: openerp.netsvc.log(rpc_response, logging.DEBUG, logline, result) @@ -538,7 +545,36 @@ class JsonRequest(WebRequest): if self.jsonp_handler: return self.jsonp_handler() try: + rpc_request_flag = rpc_request.isEnabledFor(logging.DEBUG) + rpc_response_flag = rpc_response.isEnabledFor(logging.DEBUG) + if rpc_request_flag or rpc_response_flag: + endpoint = self.endpoint.method.__name__ + model = self.params.get('model') + method = self.params.get('method') + args = self.params.get('args', []) + + start_time = time.time() + _, start_vms = 0, 0 + if psutil: + _, start_vms = psutil.Process().get_memory_info() + if rpc_request and rpc_response_flag: + rpc_request.debug('%s: %s %s, %s', + endpoint, model, method, pprint.pformat(args)) + result = self._call_function(**self.params) + + if rpc_request_flag or rpc_response_flag: + end_time = time.time() + _, end_vms = 0, 0 + if psutil: + _, end_vms = psutil.Process().get_memory_info() + logline = '%s: %s %s: time:%.3fs mem: %sk -> %sk (diff: %sk)' % ( + endpoint, model, method, end_time - start_time, start_vms / 1024, end_vms / 1024, (end_vms - start_vms)/1024) + if rpc_response_flag: + rpc_response.debug('%s, %s', logline, pprint.pformat(result)) + else: + rpc_request.debug(logline) + return self._json_response(result) except Exception, e: return self._handle_exception(e) diff --git a/openerp/models.py b/openerp/models.py index e558571d902..eec8f99e17b 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -4177,7 +4177,7 @@ class BaseModel(object): # invalidate and mark new-style fields to recompute; do this before # setting other fields, because it can require the value of computed # fields, e.g., a one2many checking constraints on records - recs.modified([u[0] for u in updates]) + recs.modified(self._fields) # call the 'set' method of fields which are not classic_write upd_todo.sort(lambda x, y: self._columns[x].priority-self._columns[y].priority) @@ -5928,10 +5928,10 @@ def itemgetter_tuple(items): def convert_pgerror_23502(model, fields, info, e): m = re.match(r'^null value in column "(?P<field>\w+)" violates ' r'not-null constraint\n', - str(e)) + tools.ustr(e)) field_name = m and m.group('field') if not m or field_name not in fields: - return {'message': unicode(e)} + return {'message': tools.ustr(e)} message = _(u"Missing required value for the field '%s'.") % field_name field = fields.get(field_name) if field: @@ -5943,10 +5943,10 @@ def convert_pgerror_23502(model, fields, info, e): def convert_pgerror_23505(model, fields, info, e): m = re.match(r'^duplicate key (?P<field>\w+) violates unique constraint', - str(e)) + tools.ustr(e)) field_name = m and m.group('field') if not m or field_name not in fields: - return {'message': unicode(e)} + return {'message': tools.ustr(e)} message = _(u"The value for the field '%s' already exists.") % field_name field = fields.get(field_name) if field: @@ -5959,7 +5959,7 @@ def convert_pgerror_23505(model, fields, info, e): PGERROR_TO_OE = defaultdict( # shape of mapped converters - lambda: (lambda model, fvg, info, pgerror: {'message': unicode(pgerror)}), { + lambda: (lambda model, fvg, info, pgerror: {'message': tools.ustr(pgerror)}), { # not_null_violation '23502': convert_pgerror_23502, # unique constraint error diff --git a/openerp/modules/module.py b/openerp/modules/module.py index d35777a8995..952fe5bad5e 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -22,6 +22,8 @@ import functools import imp +import importlib +import inspect import itertools import logging import os @@ -392,23 +394,26 @@ def adapt_version(version): return version def get_test_modules(module): - """ Return a list of module for the addons potentialy containing tests to + """ Return a list of module for the addons potentially containing tests to feed unittest2.TestLoader.loadTestsFromModule() """ # Try to import the module - module = 'openerp.addons.' + module + '.tests' + modpath = 'openerp.addons.' + module try: - __import__(module) + mod = importlib.import_module('.tests', modpath) except Exception, e: # If module has no `tests` sub-module, no problem. if str(e) != 'No module named tests': _logger.exception('Can not `import %s`.', module) return [] - # include submodules too - result = [mod_obj for name, mod_obj in sys.modules.iteritems() - if mod_obj # mod_obj can be None - if name.startswith(module) - if re.search(r'test_\w+$', name)] + if hasattr(mod, 'fast_suite') or hasattr(mod, 'checks'): + _logger.warn( + "Found deprecated fast_suite or checks attribute in test module " + "%s. These have no effect in or after version 8.0.", + mod.__name__) + + result = [mod_obj for name, mod_obj in inspect.getmembers(mod, inspect.ismodule) + if name.startswith('test_')] return result # Use a custom stream object to log the test executions. diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 160d349addf..f890d23a5bd 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -173,7 +173,7 @@ def init_logger(): def is_a_tty(stream): return hasattr(stream, 'fileno') and os.isatty(stream.fileno()) - if isinstance(handler, logging.StreamHandler) and is_a_tty(handler.stream): + if os.name == 'posix' and isinstance(handler, logging.StreamHandler) and is_a_tty(handler.stream): formatter = ColoredFormatter(format) else: formatter = DBFormatter(format) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 453566280c5..0dbe417b814 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -1208,7 +1208,7 @@ class expression(object): format = need_wildcard and '%s' or model._columns[left]._symbol_set[0] unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x column = '%s.%s' % (table_alias, _quote(left)) - query = '(%s%s %s %s)' % (unaccent(column), cast, sql_operator, unaccent(format)) + query = '(%s %s %s)' % (unaccent(column + cast), sql_operator, unaccent(format)) elif left in MAGIC_COLUMNS: query = "(%s.\"%s\"%s %s %%s)" % (table_alias, left, cast, sql_operator) params = right diff --git a/openerp/report/render/rml2pdf/customfonts.py b/openerp/report/render/rml2pdf/customfonts.py index eac44d13ba1..dc9f0c41466 100644 --- a/openerp/report/render/rml2pdf/customfonts.py +++ b/openerp/report/render/rml2pdf/customfonts.py @@ -73,7 +73,7 @@ def list_all_sysfonts(): # TTFOpenFile is not very good at it. searchpath = list(set(TTFSearchPath + rl_config.TTFSearchPath)) for dirname in searchpath: - for filename in glob.glob(os.path.join(os.path.expanduser(dirname), '*.[Tt][Tt][Ff]')): + for filename in glob.glob(os.path.join(os.path.expanduser(dirname), '*.[Tt][Tt][FfCc]')): filepath.append(filename) return filepath diff --git a/openerp/service/db.py b/openerp/service/db.py index 36d0496bd7b..930b0e25f0a 100644 --- a/openerp/service/db.py +++ b/openerp/service/db.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- -from contextlib import closing -from functools import wraps + +import json import logging import os import shutil +import tempfile import threading import traceback -import tempfile import zipfile +from functools import wraps +from contextlib import closing + import psycopg2 import openerp @@ -143,71 +146,68 @@ def exp_drop(db_name): shutil.rmtree(fs) return True -def _set_pg_password_in_environment(func): - """ On systems where pg_restore/pg_dump require an explicit - password (i.e. when not connecting via unix sockets, and most - importantly on Windows), it is necessary to pass the PG user - password in the environment or in a special .pgpass file. - - This decorator handles setting - :envvar:`PGPASSWORD` if it is not already - set, and removing it afterwards. - - See also http://www.postgresql.org/docs/8.4/static/libpq-envars.html - - .. note:: This is not thread-safe, and should never be enabled for - SaaS (giving SaaS users the super-admin password is not a good idea - anyway) - """ - @wraps(func) - def wrapper(*args, **kwargs): - if os.environ.get('PGPASSWORD') or not openerp.tools.config['db_password']: - return func(*args, **kwargs) - else: - os.environ['PGPASSWORD'] = openerp.tools.config['db_password'] - try: - return func(*args, **kwargs) - finally: - del os.environ['PGPASSWORD'] - return wrapper - def exp_dump(db_name, format): with tempfile.TemporaryFile() as t: dump_db(db_name, t, format) t.seek(0) return t.read().encode('base64') -@_set_pg_password_in_environment -def dump_db(db, stream, format): - """Dump database `db` into file-like object `stream`""" - with openerp.tools.osutil.tempdir() as dump_dir: - registry = openerp.modules.registry.RegistryManager.get(db) - if format == 'zip': +def dump_db_manifest(cr): + pg_version = "%d.%d" % divmod(cr._obj.connection.server_version / 100, 100) + env = openerp.api.Environment(cr, SUPERUSER_ID, {}) + modules = dict([(i.name,i.latest_version) for i in env['ir.module.module'].search([('state','=','installed')])]) + manifest = { + 'odoo_dump': '1', + 'db_name': cr.dbname, + 'version': openerp.release.version, + 'version_info': openerp.release.version_info, + 'major_version': openerp.release.major_version, + 'pg_version': pg_version, + 'modules': modules, + } + return manifest + +def dump_db(db_name, stream, backup_format='zip'): + """Dump database `db` into file-like object `stream` if stream is None + return a file object with the dump """ + + _logger.info('DUMP DB: %s format %s', db_name, backup_format) + + cmd = ['pg_dump', '--no-owner'] + if openerp.tools.config['db_user']: + cmd.append('--username=' + openerp.tools.config['db_user']) + if openerp.tools.config['db_host']: + cmd.append('--host=' + openerp.tools.config['db_host']) + if openerp.tools.config['db_port']: + cmd.append('--port=' + str(openerp.tools.config['db_port'])) + cmd.append(db_name) + + if backup_format == 'zip': + with openerp.tools.osutil.tempdir() as dump_dir: + registry = openerp.modules.registry.RegistryManager.get(db_name) with registry.cursor() as cr: filestore = registry['ir.attachment']._filestore(cr, SUPERUSER_ID) if os.path.exists(filestore): shutil.copytree(filestore, os.path.join(dump_dir, 'filestore')) - - dump_file = os.path.join(dump_dir, 'dump.sql') - cmd = ['pg_dump', '--format=p', '--no-owner', '--file=' + dump_file] - if openerp.tools.config['db_user']: - cmd.append('--username=' + openerp.tools.config['db_user']) - if openerp.tools.config['db_host']: - cmd.append('--host=' + openerp.tools.config['db_host']) - if openerp.tools.config['db_port']: - cmd.append('--port=' + str(openerp.tools.config['db_port'])) - cmd.append(db) - - if openerp.tools.exec_pg_command(*cmd): - _logger.error('DUMP DB: %s failed! Please verify the configuration of the database ' - 'password on the server. You may need to create a .pgpass file for ' - 'authentication, or specify `db_password` in the server configuration ' - 'file.', db) - raise Exception("Couldn't dump database") - - openerp.tools.osutil.zip_dir(dump_dir, stream, include_dir=False) - - _logger.info('DUMP DB successful: %s', db) + manifest = dump_db_manifest(cr) + with open(os.path.join(dump_dir, 'manifest.json'), 'w') as fh: + json.dump(manifest, fh, indent=4) + cmd.insert(-1, '--file=' + os.path.join(dump_dir, 'dump.sql')) + openerp.tools.exec_pg_command(*cmd) + if stream: + openerp.tools.osutil.zip_dir(dump_dir, stream, include_dir=False) + else: + t=tempfile.TemporaryFile() + openerp.tools.osutil.zip_dir(dump_dir, t, include_dir=False) + t.seek(0) + return t + else: + cmd.insert(-1, '--format=c') + stdin, stdout = openerp.tools.exec_pg_command_pipe(*cmd) + if stream: + shutil.copyfileobj(stdout, stream) + else: + return stdout def exp_restore(db_name, data, copy=False): data_file = tempfile.NamedTemporaryFile(delete=False) @@ -219,7 +219,6 @@ def exp_restore(db_name, data, copy=False): os.unlink(data_file.name) return True -@_set_pg_password_in_environment def restore_db(db, dump_file, copy=False): assert isinstance(db, basestring) if exp_db_exist(db): diff --git a/openerp/service/server.py b/openerp/service/server.py index c43afb46775..40ab85e44b3 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -7,7 +7,6 @@ import logging import os import os.path import platform -import psutil import random import select import signal @@ -25,6 +24,7 @@ if os.name == 'posix': # Unix only for workers import fcntl import resource + import psutil else: # Windows shim signal.SIGHUP = -1 diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 32a15423c95..26dc7661b5d 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -290,7 +290,7 @@ class HttpCase(TransactionCase): _logger.info('phantomjs unlink localstorage %s', i) os.unlink(i) try: - phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None) except OSError: raise unittest2.SkipTest("PhantomJS not found") try: diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 93f877f5050..eca4d2a5222 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -203,7 +203,7 @@ class configmanager(object): ] group.add_option('--log-level', dest='log_level', type='choice', choices=levels, my_default='info', - help='specify the level of the logging. Accepted values: %s (deprecated option).' % (levels,)) + help='specify the level of the logging. Accepted values: %s.' % (levels,)) parser.add_option_group(group) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 9d15eb08b61..11d0ebb7088 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -65,12 +65,34 @@ _logger = logging.getLogger(__name__) # We include the *Base ones just in case, currently they seem to be subclasses of the _* ones. SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase) +#---------------------------------------------------------- +# Subprocesses +#---------------------------------------------------------- + def find_in_path(name): path = os.environ.get('PATH', os.defpath).split(os.pathsep) if config.get('bin_path') and config['bin_path'] != 'None': path.append(config['bin_path']) return which(name, path=os.pathsep.join(path)) +def _exec_pipe(prog, args, env=None): + cmd = (prog,) + args + # on win32, passing close_fds=True is not compatible + # with redirecting std[in/err/out] + close_fds = os.name=="posix" + pop = subprocess.Popen(cmd, bufsize=-1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=close_fds, env=env) + return pop.stdin, pop.stdout + +def exec_command_pipe(name, *args): + prog = find_in_path(name) + if not prog: + raise Exception('Command `%s` not found.' % name) + _exec_pipe(prog, *args) + +#---------------------------------------------------------- +# Postgres subprocesses +#---------------------------------------------------------- + def find_pg_tool(name): path = None if config['pg_path'] and config['pg_path'] != 'None': @@ -78,38 +100,33 @@ def find_pg_tool(name): try: return which(name, path=path) except IOError: - return None + raise Exception('Command `%s` not found.' % name) + +def exec_pg_environ(): + """ On systems where pg_restore/pg_dump require an explicit password (i.e. + on Windows where TCP sockets are used), it is necessary to pass the + postgres user password in the PGPASSWORD environment variable or in a + special .pgpass file. + + See also http://www.postgresql.org/docs/8.4/static/libpq-envars.html + """ + env = os.environ.copy() + if not env.get('PGPASSWORD') and openerp.tools.config['db_password']: + env['PGPASSWORD'] = openerp.tools.config['db_password'] + return env def exec_pg_command(name, *args): prog = find_pg_tool(name) - if not prog: - raise Exception('Couldn\'t find %s' % name) - args2 = (prog,) + args - + env = exec_pg_environ() with open(os.devnull) as dn: - return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT) + rc = subprocess.call((prog,) + args, stdout=dn, stderr=subprocess.STDOUT) + if rc: + raise Exception('Postgres subprocess %s error %s' % (args2, rc)) def exec_pg_command_pipe(name, *args): prog = find_pg_tool(name) - if not prog: - raise Exception('Couldn\'t find %s' % name) - # on win32, passing close_fds=True is not compatible - # with redirecting std[in/err/out] - pop = subprocess.Popen((prog,) + args, bufsize= -1, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - close_fds=(os.name=="posix")) - return pop.stdin, pop.stdout - -def exec_command_pipe(name, *args): - prog = find_in_path(name) - if not prog: - raise Exception('Couldn\'t find %s' % name) - # on win32, passing close_fds=True is not compatible - # with redirecting std[in/err/out] - pop = subprocess.Popen((prog,) + args, bufsize= -1, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - close_fds=(os.name=="posix")) - return pop.stdin, pop.stdout + env = exec_pg_environ() + return _exec_pipe(prog, args, env) #---------------------------------------------------------- # File paths diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 770ccfd4f7d..42e9b05cdba 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -358,6 +358,7 @@ class TinyPoFile(object): source = unquote(line[6:]) line = self.lines.pop(0).strip() if not source and self.first: + self.first = False # if the source is "" and it's the first msgid, it's the special # msgstr with the informations about the traduction and the # traductor; we skip it @@ -387,8 +388,6 @@ class TinyPoFile(object): if t == trans_type == 'code': continue self.extra_lines.append((t, n, r, source, trad, comments)) - self.first = False - if name is None: if not fuzzy: self.warn('Missing "#:" formated comment at line %d for the following source:\n\t%s', |
