diff --git a/addons/account/__init__.py b/addons/account/__init__.py index 36cc1c85b5a..a243b94c2da 100644 --- a/addons/account/__init__.py +++ b/addons/account/__init__.py @@ -35,5 +35,5 @@ import product import ir_sequence import company import res_currency - +import edi # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account/__openerp__.py b/addons/account/__openerp__.py index 9445688ec6d..81d09b30ae9 100644 --- a/addons/account/__openerp__.py +++ b/addons/account/__openerp__.py @@ -53,7 +53,7 @@ module named account_voucher. 'website': 'http://www.openerp.com', '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'], 'init_xml': [], - "depends" : ["base_setup", "product", "analytic", "process","board"], + "depends" : ["base_setup", "product", "analytic", "process", "board", "edi"], 'update_xml': [ 'security/account_security.xml', 'security/ir.model.access.csv', @@ -123,7 +123,8 @@ module named account_voucher. 'board_account_view.xml', "wizard/account_report_profit_loss_view.xml", "wizard/account_report_balance_sheet_view.xml", - "account_bank_view.xml" + "edi/invoice_action_data.xml", + "account_bank_view.xml", ], 'demo_xml': [ 'demo/account_demo.xml', @@ -145,10 +146,9 @@ module named account_voucher. 'test/account_fiscalyear_close.yml', 'test/account_bank_statement.yml', 'test/account_cash_statement.yml', + 'test/test_edi_invoice.yml', 'test/account_report.yml', - - - ], + ], 'installable': True, 'active': False, 'certificate': '0080331923549', diff --git a/addons/account/account_report.xml b/addons/account/account_report.xml index 32d10ba0153..9bb40a7a503 100644 --- a/addons/account/account_report.xml +++ b/addons/account/account_report.xml @@ -19,6 +19,7 @@ rml="account/report/account_print_invoice.rml" string="Invoices" attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/',''))" + usage="default" multi="True"/> diff --git a/addons/account/company.py b/addons/account/company.py index c903b6d4a71..4ad4d1226d7 100644 --- a/addons/account/company.py +++ b/addons/account/company.py @@ -24,6 +24,7 @@ from osv import fields, osv class res_company(osv.osv): _inherit = "res.company" _columns = { + 'paypal_account': fields.char("Paypal Account", size=128, help="Paypal username (usually email) for receiving online payments."), 'overdue_msg': fields.text('Overdue Payments Message', translate=True), 'property_reserve_and_surplus_account': fields.property( 'account.account', diff --git a/addons/account/company_view.xml b/addons/account/company_view.xml index 0664fadd761..0cc67f23cfe 100644 --- a/addons/account/company_view.xml +++ b/addons/account/company_view.xml @@ -24,6 +24,7 @@ + diff --git a/addons/account/edi/__init__.py b/addons/account/edi/__init__.py new file mode 100644 index 00000000000..d86e8a91bf3 --- /dev/null +++ b/addons/account/edi/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import invoice diff --git a/addons/account/edi/invoice.py b/addons/account/edi/invoice.py new file mode 100644 index 00000000000..061bf8ee97d --- /dev/null +++ b/addons/account/edi/invoice.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import fields, osv, orm +from edi import EDIMixin + +INVOICE_LINE_EDI_STRUCT = { + 'name': True, + 'origin': True, + 'uos_id': True, + 'product_id': True, + 'price_unit': True, + 'quantity': True, + 'discount': True, + 'note': True, + + # fields used for web preview only - discarded on import + 'price_subtotal': True, +} + +INVOICE_TAX_LINE_EDI_STRUCT = { + 'name': True, + 'base': True, + 'amount': True, + 'manual': True, + 'sequence': True, + 'base_amount': True, + 'tax_amount': True, +} + +INVOICE_EDI_STRUCT = { + 'name': True, + 'origin': True, + 'company_id': True, # -> to be changed into partner + 'type': True, # -> reversed at import + 'internal_number': True, # -> reference at import + 'comment': True, + 'date_invoice': True, + 'date_due': True, + 'partner_id': True, + 'payment_term': True, + #custom: currency_id + 'invoice_line': INVOICE_LINE_EDI_STRUCT, + 'tax_line': INVOICE_TAX_LINE_EDI_STRUCT, + + # fields used for web preview only - discarded on import + #custom: 'partner_ref' + 'amount_total': True, + 'amount_untaxed': True, + 'amount_tax': True, +} + +class account_invoice(osv.osv, EDIMixin): + _inherit = 'account.invoice' + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + """Exports a supplier or customer invoice""" + edi_struct = dict(edi_struct or INVOICE_EDI_STRUCT) + res_company = self.pool.get('res.company') + res_partner_address = self.pool.get('res.partner.address') + edi_doc_list = [] + for invoice in records: + # generate the main report + self._edi_generate_report_attachment(cr, uid, invoice, context=context) + edi_doc = super(account_invoice,self).edi_export(cr, uid, [invoice], edi_struct, context)[0] + edi_doc.update({ + 'company_address': res_company.edi_export_address(cr, uid, invoice.company_id, context=context), + 'company_paypal_account': invoice.company_id.paypal_account, + 'partner_address': res_partner_address.edi_export(cr, uid, [invoice.address_invoice_id], context=context)[0], + + 'currency': self.pool.get('res.currency').edi_export(cr, uid, [invoice.currency_id], context=context)[0], + 'partner_ref': invoice.reference or False, + }) + edi_doc_list.append(edi_doc) + return edi_doc_list + + def _edi_tax_account(self, cr, uid, invoice_type='out_invoice', context=None): + #TODO/FIXME: should select proper Tax Account + account_pool = self.pool.get('account.account') + account_ids = account_pool.search(cr, uid, [('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]) + tax_account = False + if account_ids: + tax_account = account_pool.browse(cr, uid, account_ids[0]) + return tax_account + + def _edi_invoice_account(self, cr, uid, partner_id, invoice_type, context=None): + partner_pool = self.pool.get('res.partner') + partner = partner_pool.browse(cr, uid, partner_id, context=context) + if invoice_type in ('out_invoice', 'out_refund'): + invoice_account = partner.property_account_receivable + else: + invoice_account = partner.property_account_payable + return invoice_account + + def _edi_product_account(self, cr, uid, product_id, invoice_type, context=None): + product_pool = self.pool.get('product.product') + product = product_pool.browse(cr, uid, product_id, context=context) + if invoice_type in ('out_invoice','out_refund'): + account = product.property_account_income or product.categ_id.property_account_income_categ + else: + account = product.property_account_expense or product.categ_id.property_account_expense_categ + return account + + def _edi_import_company(self, cr, uid, edi_document, context=None): + # TODO: for multi-company setups, we currently import the document in the + # user's current company, but we should perhaps foresee a way to select + # the desired company among the user's allowed companies + + self._edi_requires_attributes(('company_id','company_address','type'), edi_document) + res_partner_address = self.pool.get('res.partner.address') + res_partner = self.pool.get('res.partner') + + # imported company = new partner + src_company_id, src_company_name = edi_document.pop('company_id') + partner_id = self.edi_import_relation(cr, uid, 'res.partner', src_company_name, + src_company_id, context=context) + invoice_type = edi_document['type'] + partner_value = {} + if invoice_type in ('out_invoice', 'out_refund'): + partner_value.update({'customer': True}) + if invoice_type in ('in_invoice', 'in_refund'): + partner_value.update({'supplier': True}) + res_partner.write(cr, uid, [partner_id], partner_value, context=context) + + # imported company_address = new partner address + address_info = edi_document.pop('company_address') + address_info['partner_id'] = (src_company_id, src_company_name) + address_info['type'] = 'invoice' + address_id = res_partner_address.edi_import(cr, uid, address_info, context=context) + + # modify edi_document to refer to new partner + partner_address = res_partner_address.browse(cr, uid, address_id, context=context) + edi_document['partner_id'] = (src_company_id, src_company_name) + edi_document.pop('partner_address', False) # ignored + edi_document['address_invoice_id'] = self.edi_m2o(cr, uid, partner_address, context=context) + + return partner_id + + + def edi_import(self, cr, uid, edi_document, context=None): + """ During import, invoices will import the company that is provided in the invoice as + a new partner (e.g. supplier company for a customer invoice will be come a supplier + record for the new invoice. + Summary of tasks that need to be done: + - import company as a new partner, if type==in then supplier=1, else customer=1 + - partner_id field is modified to point to the new partner + - company_address data used to add address to new partner + - change type: out_invoice'<->'in_invoice','out_refund'<->'in_refund' + - reference: should contain the value of the 'internal_number' + - reference_type: 'none' + - internal number: reset to False, auto-generated + - journal_id: should be selected based on type: simply put the 'type' + in the context when calling create(), will be selected correctly + - payment_term: if set, create a default one based on name... + - for invoice lines, the account_id value should be taken from the + product's default, i.e. from the default category, as it will not + be provided. + - for tax lines, we disconnect from the invoice.line, so all tax lines + will be of type 'manual', and default accounts should be picked based + on the tax config of the DB where it is imported. + """ + if context is None: + context = {} + self._edi_requires_attributes(('company_id','company_address','type','invoice_line','currency'), edi_document) + + # extract currency info + res_currency = self.pool.get('res.currency') + currency_info = edi_document.pop('currency') + currency_id = res_currency.edi_import(cr, uid, currency_info, context=context) + currency = res_currency.browse(cr, uid, currency_id) + edi_document['currency_id'] = self.edi_m2o(cr, uid, currency, context=context) + + # change type: out_invoice'<->'in_invoice','out_refund'<->'in_refund' + invoice_type = edi_document['type'] + invoice_type = invoice_type.startswith('in_') and invoice_type.replace('in_','out_') or invoice_type.replace('out_','in_') + edi_document['type'] = invoice_type + + #import company as a new partner + partner_id = self._edi_import_company(cr, uid, edi_document, context=context) + + # Set Account + invoice_account = self._edi_invoice_account(cr, uid, partner_id, invoice_type, context=context) + edi_document['account_id'] = invoice_account and self.edi_m2o(cr, uid, invoice_account, context=context) or False + + # reference: should contain the value of the 'internal_number' + edi_document['reference'] = edi_document.get('internal_number', False) + # reference_type: 'none' + edi_document['reference_type'] = 'none' + + # internal number: reset to False, auto-generated + edi_document['internal_number'] = False + + # discard web preview fields, if present + edi_document.pop('partner_ref', None) + + # journal_id: should be selected based on type: simply put the 'type' in the context when calling create(), will be selected correctly + context.update(type=invoice_type) + + # for invoice lines, the account_id value should be taken from the product's default, i.e. from the default category, as it will not be provided. + for edi_invoice_line in edi_document['invoice_line']: + product_info = edi_invoice_line['product_id'] + product_id = self.edi_import_relation(cr, uid, 'product.product', product_info[1], + product_info[0], context=context) + account = self._edi_product_account(cr, uid, product_id, invoice_type, context=context) + # TODO: could be improved with fiscal positions perhaps + # account = fpos_obj.map_account(cr, uid, fiscal_position_id, account.id) + edi_invoice_line['account_id'] = self.edi_m2o(cr, uid, account, context=context) if account else False + + # discard web preview fields, if present + edi_invoice_line.pop('price_subtotal', None) + + # for tax lines, we disconnect from the invoice.line, so all tax lines will be of type 'manual', and default accounts should be picked based + # on the tax config of the DB where it is imported. + tax_account = self._edi_tax_account(cr, uid, context=context) + tax_account_info = self.edi_m2o(cr, uid, tax_account, context=context) + for edi_tax_line in edi_document.get('tax_line', []): + edi_tax_line['account_id'] = tax_account_info + edi_tax_line['manual'] = True + + return super(account_invoice,self).edi_import(cr, uid, edi_document, context=context) + + + def _edi_record_display_action(self, cr, uid, id, context=None): + """Returns an appropriate action definition dict for displaying + the record with ID ``rec_id``. + + :param int id: database ID of record to display + :return: action definition dict + """ + action = super(account_invoice,self)._edi_record_display_action(cr, uid, id, context=context) + try: + invoice = self.browse(cr, uid, id, context=context) + if 'out_' in invoice.type: + view_ext_id = 'invoice_form' + journal_type = 'sale' + else: + view_ext_id = 'invoice_supplier_form' + journal_type = 'purchase' + ctx = "{'type': '%s', 'journal_type': '%s'}" % (invoice.type, journal_type) + action.update(context=ctx) + view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', view_ext_id)[1] + action.update(views=[(view_id,'form'), (False, 'tree')]) + except ValueError: + # ignore if views are missing + pass + return action + + +class account_invoice_line(osv.osv, EDIMixin): + _inherit='account.invoice.line' + +class account_invoice_tax(osv.osv, EDIMixin): + _inherit = "account.invoice.tax" + + diff --git a/addons/account/edi/invoice_action_data.xml b/addons/account/edi/invoice_action_data.xml new file mode 100644 index 00000000000..0904ec6fd54 --- /dev/null +++ b/addons/account/edi/invoice_action_data.xml @@ -0,0 +1,186 @@ + + + + + + if (object.type in ('out_invoice', 'out_refund')) and not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='account.email_template_edi_invoice', context=context) + + code + ir.actions.server + + True + Auto-email confirmed invoices + + + + + Email Templates + email.template + form + form,tree + + + {'search_default_model_id':'account.invoice'} + + + + + + + + + + + + + + + + Automated Invoice Notification Mail + ${object.user_id.user_email or object.company_id.email or 'noreply@localhost'} + ${object.company_id.name} Invoice (Ref ${object.number or 'n/a' }) + ${object.address_invoice_id.email or ''} + + + + +

Hello${object.address_invoice_id.name and ' ' or ''}${object.address_invoice_id.name or ''},

+ +

A new invoice is available for ${object.partner_id.name}:

+ +

+   REFERENCES
+   Invoice number: ${object.number}
+   Invoice total: ${object.amount_total} ${object.currency_id.name}
+   Invoice date: ${object.date_invoice}
+ % if object.origin: +   Order reference: ${object.origin}
+ % endif +   Your contact: ${object.user_id.name} +

+ +

+ You can view the invoice document, download it and pay online using the following link: +

+ View Invoice + + % if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'): + <% + comp_name = quote(object.company_id.name) + inv_number = quote(object.number) + paypal_account = quote(object.company_id.paypal_account) + inv_amount = quote(str(object.amount_total)) + cur_name = quote(object.currency_id.name) + paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Invoice%%20%s&" \ + "invoice=%s&amount=%s&currency_code=%s&button_subtype=services&no_note=1&bn=OpenERP_Invoice_PayNow_%s" % \ + (paypal_account,comp_name,inv_number,inv_number,inv_amount,cur_name,cur_name) + %> +
+

It is also possible to directly pay with Paypal:

+ + + + % endif + +
+

If you have any question, do not hesitate to contact us.

+

Thank you for choosing ${object.company_id.name or 'us'}!

+
+
+
+

+ ${object.company_id.name}

+
+
+ + % if object.company_id.street: + ${object.company_id.street}
+ % endif + % if object.company_id.street2: + ${object.company_id.street2}
+ % endif + % if object.company_id.city or object.company_id.zip: + ${object.company_id.zip} ${object.company_id.city}
+ % endif + % if object.company_id.country_id: + ${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}
+ % endif +
+ % if object.company_id.phone: +
+ Phone:  ${object.company_id.phone} +
+ % endif + % if object.company_id.website: + + %endif +

+
+ + ]]>
+ '%(object.user_id.user_email) or ''} + +You can view the invoice document, download it and pay online using the following link: + ${ctx.get('edi_web_url_view') or 'n/a'} + +% if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'): +<% +comp_name = quote(object.company_id.name) +inv_number = quote(object.number) +paypal_account = quote(object.company_id.paypal_account) +inv_amount = quote(str(object.amount_total)) +cur_name = quote(object.currency_id.name) +paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Invoice%%20%s"\ + "&invoice=%s&amount=%s¤cy_code=%s&button_subtype=services&no_note=1&bn=OpenERP_Invoice_PayNow_%s" % \ + (paypal_account,comp_name,inv_number,inv_number,inv_amount,cur_name,cur_name) +%> +It is also possible to directly pay with Paypal: + ${paypal_url} +% endif + +If you have any question, do not hesitate to contact us. + + +Thank you for choosing ${object.company_id.name}! + + +-- +${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''} +${object.company_id.name} +% if object.company_id.street: +${object.company_id.street or ''} +% endif +% if object.company_id.street2: +${object.company_id.street2} +% endif +% if object.company_id.city or object.company_id.zip: +${object.company_id.zip or ''} ${object.company_id.city or ''} +% endif +% if object.company_id.country_id: +${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''} +% endif +% if object.company_id.phone: +Phone: ${object.company_id.phone} +% endif +% if object.company_id.website: +${object.company_id.website or ''} +% endif + ]]> +
+
+
diff --git a/addons/account/test/test_edi_invoice.yml b/addons/account/test/test_edi_invoice.yml new file mode 100644 index 00000000000..b1e73140581 --- /dev/null +++ b/addons/account/test/test_edi_invoice.yml @@ -0,0 +1,152 @@ +- + In order to test the EDI export features of Invoices +- + First I create a draft customer invoice +- + !record {model: account.invoice, id: invoice_edi_1}: + journal_id: 1 + partner_id: base.res_partner_agrolait + currency_id: base.EUR + address_invoice_id: base.res_partner_address_8invoice + company_id: 1 + account_id: account.a_pay + date_invoice: '2011-06-22' + name: selling product + type: 'out_invoice' + invoice_line: + - product_id: product.product_product_pc1 + uos_id: 1 + quantity: 1.0 + price_unit: 10.0 + name: 'basic pc' + account_id: account.a_pay + invoice_line: + - product_id: product.product_product_pc3 + uos_id: 1 + quantity: 5.0 + price_unit: 100.0 + name: 'Medium PC' + account_id: account.a_pay + tax_line: + - name: sale tax + account_id: account.a_pay + manual: True + amount: 1000.00 +- + I confirm and open the invoice +- + !workflow {model: account.invoice, ref: invoice_edi_1, action: invoice_open} +- + Then I export the customer invoice +- + !python {model: edi.document}: | + invoice_pool = self.pool.get('account.invoice') + invoice = invoice_pool.browse(cr, uid, ref("invoice_edi_1")) + token = self.export_edi(cr, uid, [invoice]) + assert token, 'Invalid EDI Token' +- + Then I import a sample EDI document of another customer invoice +- + !python {model: account.invoice}: | + edi_document = { + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.random_invoice_763jsms", + "__module": "account", + "__model": "account.invoice", + "__version": [6,1,0], + "internal_number": "SAJ/2011/002", + "company_address": { + "__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.main_address", + "__module": "base", + "__model": "res.partner.address", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.be", "Belgium"], + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "bank_ids": [ + ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 123465789-156113"] + ], + }, + "company_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_company_test11", "Thomson pvt. ltd."], + "currency": { + "__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.EUR", + "__module": "base", + "__model": "res.currency", + "code": "EUR", + "symbol": "€", + }, + "partner_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_test20", "Junjun wala"], + "partner_address": { + "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh", + "__module": "base", + "__model": "res.partner.address", + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"], + }, + "date_invoice": "2011-06-22", + "name": "sample invoice", + "tax_line": [{ + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_tax-4g4EutbiEMVl", + "__module": "account", + "__model": "account.invoice.tax", + "amount": 1000.0, + "manual": True, + "name": "sale tax", + }], + "type": "out_invoice", + "invoice_line": [{ + "__module": "account", + "__model": "account.invoice.line", + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-1RP3so", + "uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "PCE"], + "name": "Basic PC", + "price_unit": 10.0, + "product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_pc1", "[PC1] Basic PC"], + "quantity": 1.0 + }, + { + "__module": "account", + "__model": "account.invoice.line", + "__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-u2XV5", + "uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "PCE"], + "name": "Medium PC", + "price_unit": 100.0, + "product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_pc3", "[PC3] Medium PC"], + "quantity": 5.0 + }] + } + invoice_id = self.edi_import(cr, uid, edi_document, context=context) + assert invoice_id, 'EDI import failed' + invoice_new = self.browse(cr, uid, invoice_id) + + # check bank info on partner + assert len(invoice_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner" + bank_info = invoice_new.partner_id.bank_ids[0] + assert bank_info.acc_number == "Sample bank: 123465789-156113", 'Expected "Sample bank: 123465789-156113", got %s' % bank_info.acc_number + + assert invoice_new.partner_id.supplier, 'Imported Partner is not marked as supplier' + assert invoice_new.reference == "SAJ/2011/002", "internal number is not stored in reference" + assert invoice_new.reference_type == 'none', "reference type is not set to 'none'" + assert invoice_new.internal_number == False, "internal number is not reset" + assert invoice_new.journal_id.id, "journal id is not selected" + assert invoice_new.type == 'in_invoice', "Invoice type was not set properly" + assert len(invoice_new.invoice_line) == 2, "invoice lines are not same" + for inv_line in invoice_new.invoice_line: + if inv_line.name == 'Basic PC': + assert inv_line.uos_id.name == "PCE" , "uom is not same" + assert inv_line.price_unit == 10 , "price unit is not same" + assert inv_line.quantity == 1 , "product qty is not same" + assert inv_line.price_subtotal == 10, "price sub total is not same" + elif inv_line.name == 'Medium PC': + assert inv_line.uos_id.name == "PCE" , "uom is not same" + assert inv_line.price_unit == 100 , "price unit is not same" + assert inv_line.quantity == 5 , "product qty is not same" + assert inv_line.price_subtotal == 500, "price sub total is not same" + else: + raise AssertionError('unknown invoice line: %s' % inv_line) + for inv_tax in invoice_new.tax_line: + assert inv_tax.manual, "tax line not set to manual" + assert inv_tax.account_id, "missing tax line account" diff --git a/addons/account/wizard/__init__.py b/addons/account/wizard/__init__.py index d82eff62142..88ae17f3c7b 100644 --- a/addons/account/wizard/__init__.py +++ b/addons/account/wizard/__init__.py @@ -66,6 +66,7 @@ import account_change_currency import account_report_balance_sheet import account_report_profit_loss + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account_voucher/test/case1_usd_usd.yml b/addons/account_voucher/test/case1_usd_usd.yml index 6685e64f298..b37a87405cc 100644 --- a/addons/account_voucher/test/case1_usd_usd.yml +++ b/addons/account_voucher/test/case1_usd_usd.yml @@ -49,18 +49,6 @@ company_id: base.main_company view_id: account.account_journal_bank_view -- - I create new partner Mark Strauss. -- - !record {model: res.partner, id: res_partner_strauss0}: - address: - - city: paris - country_id: base.fr - name: Mark Strauss - street: 1 rue Rockfeller - type: invoice - zip: '75016' - name: Mr. Mark Strauss - I create the first invoice on 1st January for 200 USD - @@ -80,7 +68,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_strauss0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -115,7 +103,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_strauss0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -139,21 +127,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_strauss0"), ref('bank_journal_USD'), 240.00, 2, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 240.00, 2, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 240.00, 'company_id': ref('base.main_company'), 'currency_id': ref('base.USD'), 'journal_id': ref('bank_journal_USD'), - 'partner_id': ref('res_partner_strauss0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-%m-%d"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'First payment', + 'name': 'First payment: Case 1 USD/USD', } vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-03-01'), ref('base.USD'), 240)['value']) if not res['value']['line_cr_ids']: @@ -171,7 +159,7 @@ I check that writeoff amount computed is 10.0 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.writeoff_amount == 10.0), "Writeoff amount is not 10.0" - @@ -179,14 +167,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my first voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -200,7 +188,7 @@ I check that my write-off is correct. 9 debit and 10 amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -237,7 +225,7 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_strauss0"), ref('bank_journal_USD'), 45.00, 2, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 45.00, 2, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 45.00, @@ -245,14 +233,14 @@ 'company_id': ref('base.main_company'), 'currency_id': ref('base.USD'), 'journal_id': ref('bank_journal_USD'), - 'partner_id': ref('res_partner_strauss0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-%m-%d"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'Second payment', + 'name': 'Second payment: Case 1', } vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 45.0)['value']) if not res['value']['line_cr_ids']: @@ -270,7 +258,7 @@ I check that writeoff amount computed is 5.0 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.writeoff_amount == 5.0), "Writeoff amount is not 5.0" - @@ -278,14 +266,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my second voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -303,7 +291,7 @@ I check that my writeoff is correct. 4.75 debit and 5 amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) diff --git a/addons/account_voucher/test/case2_suppl_usd_eur.yml b/addons/account_voucher/test/case2_suppl_usd_eur.yml index 424783d8a7b..23f11d28803 100644 --- a/addons/account_voucher/test/case2_suppl_usd_eur.yml +++ b/addons/account_voucher/test/case2_suppl_usd_eur.yml @@ -29,18 +29,6 @@ currency: base.USD company_id: base.main_company view_id: account.account_journal_bank_view -- - I create a new partner Robert Clements -- - !record {model: res.partner, id: res_partner_clements0}: - address: - - city: marseille - country_id: base.fr - name: Robert - street: 1 rue Rockfeller - type: invoice - zip: '13016' - name: Mr.Robert Clements - I create the first invoice on 1st January for 200 USD - @@ -61,7 +49,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_clements0 + partner_id: base.res_partner_seagate reference_type: none check_total : 200 - @@ -98,7 +86,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_clements0 + partner_id: base.res_partner_seagate reference_type: none check_total : 100.0 - @@ -122,21 +110,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_clements0"), ref('bank_journal_EUR'), 240.0, 2, ttype='payment', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 240.0, 2, ttype='payment', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 240.0, 'company_id': ref('base.main_company'), 'currency_id': ref('base.EUR'), 'journal_id': ref('bank_journal_EUR'), - 'partner_id': ref('res_partner_clements0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'payment', 'date': time.strftime("%Y-03-01"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'First payment', + 'name': 'First payment: Case 2 SUPPL USD/EUR', } if not res['value']['line_dr_ids']: res['value']['line_dr_ids'] = [{'type': 'dr', 'account_id': ref('account.a_pay'),}] @@ -153,14 +141,14 @@ I check that writeoff amount computed is -15.0 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.writeoff_amount == -15.0), "Writeoff amount is not -15.0" - I check that currency rate difference is 34.0 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.currency_rate_difference == 34.0), "Currency rate difference is not 34.0" - @@ -168,14 +156,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -191,7 +179,7 @@ I check that my writeoff is correct. -15 in credit with no amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -229,21 +217,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_clements0"), ref('bank_journal_EUR'), 45.0, 2, ttype='payment', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 45.0, 2, ttype='payment', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 45.0, 'company_id': ref('base.main_company'), 'currency_id': ref('base.USD'), 'journal_id': ref('bank_journal_USD'), - 'partner_id': ref('res_partner_clements0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'payment', 'date': time.strftime("%Y-%m-%d"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'Second payment', + 'name': 'Second payment: Case 2 SUPPL USD/EUR', } vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 45.0)['value']) if not res['value']['line_dr_ids']: @@ -261,14 +249,14 @@ I check that writeoff amount computed is -5.0 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.writeoff_amount == 5.0), "Writeoff amount is not 5.0" - I check that currency rate difference is 8.50 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.currency_rate_difference == 8.50), "Currency rate difference is not 8.50" - @@ -276,14 +264,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that my voucher state is posted - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert voucher_id.state == 'posted', "Voucher state is not posted" - @@ -296,7 +284,7 @@ I check that my writeoff is correct. 4.75 in credit and 5 in amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) diff --git a/addons/account_voucher/test/case2_usd_eur_debtor_in_eur.yml b/addons/account_voucher/test/case2_usd_eur_debtor_in_eur.yml index 58cf92ec460..11e1e3cc0c2 100644 --- a/addons/account_voucher/test/case2_usd_eur_debtor_in_eur.yml +++ b/addons/account_voucher/test/case2_usd_eur_debtor_in_eur.yml @@ -66,18 +66,6 @@ currency: base.USD company_id: base.main_company view_id: account.account_journal_bank_view -- - I create a new partner Michael Geller -- - !record {model: res.partner, id: res_partner_michael0}: - address: - - city: paris - country_id: base.fr - name: Michael - street: 1 rue Rockfeller - type: invoice - zip: '75016' - name: Mr.Michael Geller - I create the first invoice on 1st January for 200 USD - @@ -97,7 +85,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_michael0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -132,7 +120,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_michael0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -155,21 +143,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michael0"), ref('bank_journal_EUR'), 240.0, False, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 240.0, False, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 200.0, 'company_id': ref('base.main_company'), 'currency_id': False, 'journal_id': ref('bank_journal_EUR'), - 'partner_id': ref('res_partner_michael0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-03-01"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'First payment', + 'name': 'First payment: Case 2 USD/EUR DR EUR', } if not res['value']['line_cr_ids']: res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}] @@ -187,14 +175,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -206,7 +194,7 @@ I check that the debtor account has 2 new lines with 0 in amount_currency columns and their credit columns are 130 and 70 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -237,14 +225,14 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michael0"), ref('bank_journal_USD'), 80.0, 2, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 80.0, 2, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 80.0, 'company_id': ref('base.main_company'), 'currency_id': ref('base.USD'), 'journal_id': ref('bank_journal_USD'), - 'partner_id': ref('res_partner_michael0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-%m-%d"), @@ -252,7 +240,7 @@ 'writeoff_acc_id': ref('account.a_expense'), 'exchange_acc_id': ref('account.o_expense'), 'comment': 'Write Off', - 'name': 'Second payment', + 'name': 'Second payment: Case 2 SUPPL USD/EUR DR EUR', } vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 80.0)['value']) if not res['value']['line_cr_ids']: @@ -270,7 +258,7 @@ I check that writeoff amount computed is 2.22 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (round(voucher_id.writeoff_amount, 2) == 2.22), "Writeoff amount is not 2.22$" - @@ -278,14 +266,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that my voucher state is posted - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert voucher_id.state == 'posted', "Voucher state is not posted" - @@ -298,7 +286,7 @@ I check that my writeoff is correct. 2.11 in credit and 2.22 in amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) diff --git a/addons/account_voucher/test/case2_usd_eur_debtor_in_usd.yml b/addons/account_voucher/test/case2_usd_eur_debtor_in_usd.yml index 355098dc7d6..c72825cb0c7 100644 --- a/addons/account_voucher/test/case2_usd_eur_debtor_in_usd.yml +++ b/addons/account_voucher/test/case2_usd_eur_debtor_in_usd.yml @@ -66,18 +66,6 @@ currency: base.USD company_id: base.main_company view_id: account.account_journal_bank_view -- - I create a new partner Michal Gallette -- - !record {model: res.partner, id: res_partner_michal1}: - address: - - city: paris - country_id: base.fr - name: Michal - street: 1 rue Rockfeller - type: invoice - zip: '75016' - name: Mr.Michal Gallette - I create the first invoice on 1st January for 200 USD - @@ -97,7 +85,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_michal1 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -132,7 +120,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_michal1 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -155,21 +143,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michal1"), ref('bank_journal_EUR'), 200.0, False, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 200.0, False, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 200.0, 'company_id': ref('base.main_company'), 'currency_id': ref('base.EUR'), 'journal_id': ref('bank_journal_EUR'), - 'partner_id': ref('res_partner_michal1'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-03-01"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'First payment', + 'name': 'First payment: Case 2 USD/EUR DR USD', } if not res['value']['line_cr_ids']: res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}] @@ -187,14 +175,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -206,7 +194,7 @@ I check that the debtor account has 2 new lines with 144.44 and 77.78 in amount_currency columns and their credit columns are 130 and 70 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -240,14 +228,14 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michal1"), ref('bank_journal_USD'), 80.0, ref('base.USD'), ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 80.0, ref('base.USD'), ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 80.0, 'company_id': ref('base.main_company'), 'currency_id': ref('base.USD'), 'journal_id': ref('bank_journal_USD'), - 'partner_id': ref('res_partner_michal1'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-%m-%d"), @@ -255,7 +243,7 @@ 'writeoff_acc_id': ref('account.a_expense'), 'exchange_acc_id': ref('account.o_expense'), 'comment': 'Write Off', - 'name': 'Second payment', + 'name': 'Second payment: Case 2 SUPPL USD/EUR DR USD', } vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 80.0)['value']) if not res['value']['line_cr_ids']: @@ -273,7 +261,7 @@ I check that writeoff amount computed is 2.22 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (round(voucher_id.writeoff_amount, 2) == 2.22), "Writeoff amount is not 2.22$" - @@ -281,14 +269,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that my voucher state is posted - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert voucher_id.state == 'posted', "Voucher state is not posted" - @@ -301,7 +289,7 @@ I check that my writeoff is correct. 2.11 in credit and 2.22 in amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) diff --git a/addons/account_voucher/test/case3_eur_eur.yml b/addons/account_voucher/test/case3_eur_eur.yml index 9ed498442fb..5649bf0d4bb 100644 --- a/addons/account_voucher/test/case3_eur_eur.yml +++ b/addons/account_voucher/test/case3_eur_eur.yml @@ -29,18 +29,6 @@ currency: base.EUR company_id: base.main_company view_id: account.account_journal_bank_view -- - I create a new partner Peter Lawson. -- - !record {model: res.partner, id: res_partner_peter0}: - address: - - city: paris - country_id: base.fr - name: Peter - street: 1 rue Rockfeller - type: invoice - zip: '75016' - name: Mr.Peter Lawson - I create the first invoice on 1st January for 150 EUR - @@ -60,7 +48,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_peter0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -95,7 +83,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_peter0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -118,21 +106,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_peter0"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 120.00, 'company_id': ref('base.main_company'), 'currency_id': ref('base.EUR'), 'journal_id': ref('bank_journal_EUR'), - 'partner_id': ref('res_partner_peter0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-03-01"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'First payment', + 'name': 'First payment: Case 3', } if not res['value']['line_cr_ids']: res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}] @@ -149,7 +137,7 @@ I check that writeoff amount computed is 0.00 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.writeoff_amount == 0.00), "Writeoff amount is not 0.00" - @@ -157,14 +145,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my first voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -176,7 +164,7 @@ I check that the debtor account has 2 new lines with 0.00 and 0.00 in amount_currency columns and their credit are 20 and 100 respectively - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -210,21 +198,21 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_peter0"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 120.00, 'company_id': ref('base.main_company'), 'currency_id': ref('base.EUR'), 'journal_id': ref('bank_journal_EUR'), - 'partner_id': ref('res_partner_peter0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-04-01"), 'payment_option': 'with_writeoff', 'writeoff_acc_id': ref('account.a_expense'), 'comment': 'Write Off', - 'name': 'Second payment', + 'name': 'Second payment: Case 3', } if not res['value']['line_cr_ids']: res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}] @@ -241,7 +229,7 @@ I check that writeoff amount computed is 0.00 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'),('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (voucher_id.writeoff_amount == 0.00), "Writeoff amount is not 0" - @@ -249,14 +237,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my second voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -268,7 +256,7 @@ I check that the debtor account has 2 new lines with 0.00 and 0.00 in amount_currency columns and their credit are 70 and 50 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_peter0'))]) + voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) diff --git a/addons/account_voucher/test/case4_cad_chf.yml b/addons/account_voucher/test/case4_cad_chf.yml index 557e20dec1a..972b09702be 100644 --- a/addons/account_voucher/test/case4_cad_chf.yml +++ b/addons/account_voucher/test/case4_cad_chf.yml @@ -43,18 +43,6 @@ currency: base.CHF company_id: base.main_company view_id: account.account_journal_bank_view -- - I create a new partner John Armani. -- - !record {model: res.partner, id: res_partner_john0}: - address: - - city: paris - country_id: base.fr - name: John - street: 1 rue Rockfeller - type: invoice - zip: '75016' - name: Mr.John Armani - I create the first invoice on 1st January for 200 CAD - @@ -74,7 +62,7 @@ product_id: product.product_product_pc1 uos_id: product.product_uom_unit journal_id: account.sales_journal - partner_id: res_partner_john0 + partner_id: base.res_partner_seagate reference_type: none - I Validate invoice by clicking on Validate button @@ -97,14 +85,14 @@ !python {model: account.voucher}: | import netsvc, time vals = {} - res = self.onchange_partner_id(cr, uid, [], ref("res_partner_john0"), ref('bank_journal_CHF'), 200.00, ref('base.CHF'), ttype='receipt', date=False) + res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_CHF'), 200.00, ref('base.CHF'), ttype='receipt', date=False) vals = { 'account_id': ref('account.cash'), 'amount': 200.00, 'company_id': ref('base.main_company'), 'currency_id': ref('base.CHF'), 'journal_id': ref('bank_journal_CHF'), - 'partner_id': ref('res_partner_john0'), + 'partner_id': ref('base.res_partner_seagate'), 'period_id': ref('account.period_3'), 'type': 'receipt', 'date': time.strftime("%Y-%m-%d"), @@ -112,7 +100,7 @@ 'writeoff_acc_id': ref('account.a_expense'), 'exchange_acc_id': ref('account.o_expense'), 'comment': 'Write Off', - 'name': 'First payment', + 'name': 'First payment: Case 4', } vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-03-01'), ref('base.CHF'), 200.0)['value']) if not res['value']['line_cr_ids']: @@ -128,7 +116,7 @@ I check that writeoff amount computed is 13.26 - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) assert (round(voucher_id.writeoff_amount,2) == 13.26), "Writeoff amount is not 13.26 CHF" - @@ -136,14 +124,14 @@ - !python {model: account.voucher}: | import netsvc - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))]) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr) - I check that the move of my voucher is valid - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) @@ -160,7 +148,7 @@ I check that my writeoff is correct. 11.05 credit and 13.26 amount_currency - !python {model: account.voucher}: | - voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))]) + voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))]) voucher_id = self.browse(cr, uid, voucher[0]) move_line_obj = self.pool.get('account.move.line') move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)]) diff --git a/addons/auction/test/auction.yml b/addons/auction/test/auction.yml index 72014f985ef..f970ffe86dd 100644 --- a/addons/auction/test/auction.yml +++ b/addons/auction/test/auction.yml @@ -34,21 +34,8 @@ - !record {model: account.journal, id: account.sales_journal}: analytic_journal_id: account.cose_journal_sale -- - I'm creating new Seller "Mr. Pinakin" with him email "info@mycustomer.com". -- - !record {model: res.partner, id: res_partner_mrpinakin0}: - address: - - city: Namur - country_id: base.be - phone: (+32).10.45.18.77 - street: 23, street ways - type: default - zip: '2324324' - email: 'info@mycustomer.com' - name: Mr. Pinakin -- +- I'm creating new Buyer "Mr. Patel" with his email "info@myinfobid.com". - !record {model: res.partner, id: res_partner_mrpatel0}: @@ -61,21 +48,7 @@ email: 'info@myinfobid.com' name: Mr. Patel -- - I'm creating new Buyer "Mr. Johnson" with his email "info@mrkjohnson.com". -- - !record {model: res.partner, id: res_partner_mrkjohnson0}: - address: - - city: paris - country_id: base.fr - name: Mark Johnson - street: 1 rue Rockfeller - type: invoice - zip: '75016' - email: 'info@mrkjohnson.com' - name: Mr. Mark Johnson - -- +- I'm creating new Buyer "Mr. Rahi" with his email "info@poalrahi.com". - !record {model: res.partner, id: res_partner_poalrahi0}: @@ -140,7 +113,7 @@ date_dep: !eval "'%s-08-01' %(datetime.now().year)" method: keep name: AD/006 - partner_id: res_partner_mrpinakin0 + partner_id: base.res_partner_maxtor specific_cost_ids: - account: auction.auction_expense amount: 200.0 @@ -149,7 +122,7 @@ I create a new object wooden-chair which is to be auctioned. - !record {model: auction.lots, id: auction_lots_woodenchair0}: - ach_uid: res_partner_mrkjohnson0 + ach_uid: base.res_partner_seagate artist_id: auction_artists_vincentvangogh0 auction_id: auction_dates_antiquefurnitureexhibition0 bord_vnd_id: auction_deposit_ad0 @@ -199,12 +172,12 @@ price: 3200.0 - - I create another bid for an object "wooden-chair" bid by a Mr.Johnson + I create another bid for an object "wooden-chair" bid by a Mr.Seagate - !record {model: auction.bid, id: auction_bid_bid2}: auction_id: auction_dates_antiquefurnitureexhibition0 name: bid/003 - partner_id: res_partner_mrkjohnson0 + partner_id: base.res_partner_seagate - I create a bid line. - @@ -215,7 +188,7 @@ lot_id: auction.auction_lots_woodenchair0 price: 4000.0 - - Mr. MarkJohnson bid are selected as the Finalist Bid with 4000 Euro + Mr. Seagate bids are selected as the Finalist Bid with 4000 Euro - I check that buyer price and seller price gets bound with the value - @@ -250,7 +223,7 @@ - !record {model: auction.lots.make.invoice.buyer, id: auction_lots_make_invoice_buyer_0}: amount: 3090.0 - buyer_id: res_partner_mrkjohnson0 + buyer_id: base.res_partner_seagate number: !eval "'%s/003' %(datetime.now().year)" objects: 1 - diff --git a/addons/edi/__init__.py b/addons/edi/__init__.py new file mode 100644 index 00000000000..3aa7f74ebd7 --- /dev/null +++ b/addons/edi/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import models +import edi_service +from models.edi import EDIMixin, edi_document + +# web +import controllers \ No newline at end of file diff --git a/addons/edi/__openerp__.py b/addons/edi/__openerp__.py new file mode 100644 index 00000000000..4d96780b10c --- /dev/null +++ b/addons/edi/__openerp__.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + 'name': 'Electronic Data Interchange (EDI)', + 'version': '1.0', + 'category': 'Tools', + 'complexity': "easy", + 'description': """ +Provides a common EDI platform that other Applications can use +============================================================== + +OpenERP specifies a generic EDI format for exchanging business +documents between different systems, and provides generic +mechanisms to import and export them. + +More details about OpenERP's EDI format may be found in the +technical OpenERP documentation at http://doc.openerp.com + """, + 'author': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'depends': ['base', 'email_template'], + 'data': [ + 'security/ir.model.access.csv', + ], + 'test': [ + 'test/edi_partner_test.yml', + ], + 'js': [ + 'static/src/js/edi.js', + ], + "css": [ + "static/src/css/edi.css" + ], + 'installable': True, + 'active': False, + 'certificate': '002046536359186', +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/edi/controllers/__init__.py b/addons/edi/controllers/__init__.py new file mode 100644 index 00000000000..8ee9bae18d9 --- /dev/null +++ b/addons/edi/controllers/__init__.py @@ -0,0 +1 @@ +import main diff --git a/addons/edi/controllers/main.py b/addons/edi/controllers/main.py new file mode 100644 index 00000000000..00787cdc374 --- /dev/null +++ b/addons/edi/controllers/main.py @@ -0,0 +1,84 @@ +import json +import textwrap + +import simplejson +import werkzeug.wrappers + +import web.common.http as openerpweb +import web.controllers.main + +class EDI(openerpweb.Controller): + # http://hostname:8069/edi/view?db=XXXX&token=XXXXXXXXXXX + # http://hostname:8069/edi/import_url?url=URIEncodedURL + _cp_path = "/edi" + + def template(self, req, mods='web,edi'): + self.wc = openerpweb.controllers_path['/web/webclient'] + d = {} + d["js"] = "\n".join(''%i for i in self.wc.manifest_list(req, mods, 'js')) + d["css"] = "\n".join(''%i for i in self.wc.manifest_list(req, mods, 'css')) + d["modules"] = simplejson.dumps(mods.split(',')) + return d + + @openerpweb.httprequest + def view(self, req, db, token): + d = self.template(req) + d["init"] = 'new s.edi.EdiView(null,"%s","%s").appendTo($("body"));'%(db,token) + r = web.controllers.main.html_template % d + return r + + @openerpweb.httprequest + def import_url(self, req, url): + d = self.template(req) + d["init"] = 'new s.edi.EdiImport(null,"%s").appendTo($("body"));'%(url) + r = web.controllers.main.html_template % d + return r + + @openerpweb.httprequest + def download(self, req, db, token): + result = req.session.proxy('edi').get_edi_document(db, token) + response = werkzeug.wrappers.Response( result, headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]) + return response + + @openerpweb.httprequest + def download_attachment(self, req, db, token): + result = req.session.proxy('edi').get_edi_document(db, token) + doc = json.loads(result)[0] + attachment = doc['__attachments'] and doc['__attachments'][0] + if attachment: + result = attachment["content"].decode('base64') + import email.Utils as utils + + # Encode as per RFC 2231 + filename_utf8 = attachment['file_name'] + filename_encoded = "%s=%s" % ('filename*', + utils.encode_rfc2231(filename_utf8, 'utf-8')) + response = werkzeug.wrappers.Response(result, headers=[('Content-Type', 'application/pdf'), + ('Content-Disposition', 'inline; ' + filename_encoded), + ('Content-Length', len(result))]) + return response + + @openerpweb.httprequest + def binary(self, req, db, token, field_path="company_address.logo", content_type='image/png'): + result = req.session.proxy('edi').get_edi_document(db, token) + doc = json.loads(result)[0] + for name in field_path.split("."): + doc = doc[name] + result = doc.decode('base64') + response = werkzeug.wrappers.Response(result, headers=[('Content-Type', content_type), + ('Content-Length', len(result))]) + return response + + @openerpweb.jsonrequest + def get_edi_document(self, req, db, token): + result = req.session.proxy('edi').get_edi_document(db, token) + return json.loads(result) + + @openerpweb.jsonrequest + def import_edi_url(self, req, url): + result = req.session.proxy('edi').import_edi_url(req.session._db, req.session._uid, req.session._password, url) + if len(result) == 1: + return {"action": web.controllers.main.clean_action(req, result[0][2])} + return True + +# diff --git a/addons/edi/edi_service.py b/addons/edi/edi_service.py new file mode 100644 index 00000000000..296b3006525 --- /dev/null +++ b/addons/edi/edi_service.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import logging + +import netsvc +import openerp + +_logger = logging.getLogger('edi.service') + +class edi(netsvc.ExportService): + + def __init__(self, name="edi"): + netsvc.ExportService.__init__(self, name) + + def _edi_dispatch(self, db_name, method_name, *method_args): + try: + registry = openerp.modules.registry.RegistryManager.get(db_name) + assert registry, 'Unknown database %s' % db_name + edi_document = registry['edi.document'] + cr = registry.db.cursor() + res = None + res = getattr(edi_document, method_name)(cr, *method_args) + cr.commit() + except Exception: + _logger.exception('Failed to execute EDI method %s with args %r', method_name, method_args) + raise + finally: + cr.close() + return res + + def exp_get_edi_document(self, db_name, edi_token): + return self._edi_dispatch(db_name, 'get_document', 1, edi_token) + + def exp_import_edi_document(self, db_name, uid, passwd, edi_document, context=None): + return self._edi_dispatch(db_name, 'import_edi', uid, edi_document, None) + + def exp_import_edi_url(self, db_name, uid, passwd, edi_url, context=None): + return self._edi_dispatch(db_name, 'import_edi', uid, None, edi_url) + + def dispatch(self, method, params): + if method in ['import_edi_document', 'import_edi_url']: + (db, uid, passwd ) = params[0:3] + openerp.service.security.check(db, uid, passwd) + elif method in ['get_edi_document']: + # No security check for these methods + pass + else: + raise KeyError("Method not found: %s" % method) + fn = getattr(self, 'exp_'+method) + return fn(*params) + +edi() \ No newline at end of file diff --git a/addons/edi/models/__init__.py b/addons/edi/models/__init__.py new file mode 100644 index 00000000000..cf8c0ff2eb0 --- /dev/null +++ b/addons/edi/models/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import edi +import res_partner +import res_company +import res_currency \ No newline at end of file diff --git a/addons/edi/models/edi.py b/addons/edi/models/edi.py new file mode 100644 index 00000000000..b8fc8699ca2 --- /dev/null +++ b/addons/edi/models/edi.py @@ -0,0 +1,684 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import base64 +import hashlib +import json +import logging +import re +import threading +import time +import urllib2 + +import openerp +import openerp.release as release +import netsvc +import pooler +from osv import osv,fields,orm +from tools.translate import _ +from tools.safe_eval import safe_eval as eval + +EXTERNAL_ID_PATTERN = re.compile(r'^([^.:]+)(?::([^.]+))?\.(\S+)$') +EDI_VIEW_WEB_URL = '%s/edi/view?debug=1&db=%s&token=%s' +EDI_PROTOCOL_VERSION = 1 # arbitrary ever-increasing version number +EDI_GENERATOR = 'OpenERP ' + release.major_version +EDI_GENERATOR_VERSION = release.version_info + +def split_external_id(ext_id): + match = EXTERNAL_ID_PATTERN.match(ext_id) + assert match, \ + _("'%s' is an invalid external ID") % (ext_id) + return {'module': match.group(1), + 'db_uuid': match.group(2), + 'id': match.group(3), + 'full': match.group(0)} + +def safe_unique_id(database_id, model, record_id): + """Generate a unique string to represent a (database_uuid,model,record_id) pair + without being too long, and with a very low probability of collisions. + """ + msg = "%s-%s-%s-%s" % (time.time(), database_id, model, record_id) + digest = hashlib.sha1(msg).digest() + # fold the sha1 20 bytes digest to 9 bytes + digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2])) + # b64-encode the 9-bytes folded digest to a reasonable 12 chars ASCII ID + digest = base64.urlsafe_b64encode(digest) + return '%s-%s' % (model.replace('.','_'), digest) + +def last_update_for(record): + """Returns the last update timestamp for the given record, + if available, otherwise False + """ + if record._model._log_access: + record_log = record.perm_read()[0] + return record_log.get('write_date') or record_log.get('create_date') or False + return False + +_logger = logging.getLogger('edi') + +class edi_document(osv.osv): + _name = 'edi.document' + _description = 'EDI Document' + _columns = { + 'name': fields.char("EDI token", size = 128, help="Unique identifier for retrieving an EDI document."), + 'document': fields.text("Document", help="EDI document content") + } + _sql_constraints = [ + ('name_uniq', 'unique (name)', 'EDI Tokens must be unique!') + ] + + def new_edi_token(self, cr, uid, record): + """Return a new, random unique token to identify this model record, + and to be used as token when exporting it as an EDI document. + + :param browse_record record: model record for which a token is needed + """ + db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') + edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest() + return edi_token + + def serialize(self, edi_documents): + """Serialize the given EDI document structures (Python dicts holding EDI data), + using JSON serialization. + + :param [dict] edi_documents: list of EDI document structures to serialize + :return: UTF-8 encoded string containing the serialized document + """ + serialized_list = json.dumps(edi_documents) + return serialized_list + + def generate_edi(self, cr, uid, records, context=None): + """Generates a final EDI document containing the EDI serialization + of the given records, which should all be instances of a Model + that has the :meth:`~.edi` mixin. The document is not saved in the + database, this is done by :meth:`~.export_edi`. + + :param list(browse_record) records: records to export as EDI + :return: UTF-8 encoded string containing the serialized records + """ + edi_list = [] + for record in records: + record_model_obj = self.pool.get(record._name) + edi_list += record_model_obj.edi_export(cr, uid, [record], context=context) + return self.serialize(edi_list) + + def get_document(self, cr, uid, edi_token, context=None): + """Retrieve the EDI document corresponding to the given edi_token. + + :return: EDI document string + :raise: ValueError if requested EDI token does not match any know document + """ + _logger.debug("get_document(%s)", edi_token) + edi_ids = self.search(cr, uid, [('name','=', edi_token)], context=context) + if not edi_ids: + raise ValueError('Invalid EDI token: %s' % edi_token) + edi = self.browse(cr, uid, edi_ids[0], context=context) + return edi.document + + def load_edi(self, cr, uid, edi_documents, context=None): + """Import the given EDI document structures into the system, using + :meth:`~.import_edi`. + + :param edi_documents: list of Python dicts containing the deserialized + version of EDI documents + :return: list of (model, id, action) tuple containing the model and database ID + of all records that were imported in the system, plus a suggested + action definition dict for displaying each document. + """ + ir_module = self.pool.get('ir.module.module') + res = [] + for edi_document in edi_documents: + module = edi_document.get('__import_module') or edi_document.get('__module') + assert module, 'a `__module` or `__import_module` attribute is required in each EDI document' + if module != 'base' and not ir_module.search(cr, uid, [('name','=',module),('state','=','installed')]): + raise osv.except_osv(_('Missing Application'), + _("The document you are trying to import requires the OpenERP `%s` application. " + "You can install it by connecting as the administrator and opening the configuration assistant.")%(module,)) + model = edi_document.get('__import_model') or edi_document.get('__model') + assert model, 'a `__model` or `__import_model` attribute is required in each EDI document' + model_obj = self.pool.get(model) + assert model_obj, 'model `%s` cannot be found, despite module `%s` being available - '\ + 'this EDI document seems invalid or unsupported' % (model,module) + record_id = model_obj.edi_import(cr, uid, edi_document, context=context) + record_action = model_obj._edi_record_display_action(cr, uid, record_id, context=context) + res.append((model, record_id, record_action)) + return res + + def deserialize(self, edi_documents_string): + """Return deserialized version of the given EDI Document string. + + :param str|unicode edi_documents_string: UTF-8 string (or unicode) containing + JSON-serialized EDI document(s) + :return: Python object representing the EDI document(s) (usually a list of dicts) + """ + return json.loads(edi_documents_string) + + def export_edi(self, cr, uid, records, context=None): + """Export the given database records as EDI documents, stores them + permanently with a new unique EDI token, for later retrieval via :meth:`~.get_document`, + and returns the list of the new corresponding ``ir.edi.document`` records. + + :param records: list of browse_record of any model + :return: list of IDs of the new ``ir.edi.document`` entries, in the same + order as the provided ``records``. + """ + exported_ids = [] + for record in records: + document = self.generate_edi(cr, uid, [record], context) + token = self.new_edi_token(cr, uid, record) + self.create(cr, uid, { + 'name': token, + 'document': document + }, context=context) + exported_ids.append(token) + return exported_ids + + def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None): + """Import a JSON serialized EDI Document string into the system, first retrieving it + from the given ``edi_url`` if provided. + + :param str|unicode edi_document: UTF-8 string or unicode containing JSON-serialized + EDI Document to import. Must not be provided if + ``edi_url`` is given. + :param str|unicode edi_url: URL where the EDI document (same format as ``edi_document``) + may be retrieved, without authentication. + """ + if edi_url: + assert not edi_document, 'edi_document must not be provided if edi_url is given' + edi_document = urllib2.urlopen(edi_url).read() + assert edi_document, 'EDI Document is empty!' + edi_documents = self.deserialize(edi_document) + return self.load_edi(cr, uid, edi_documents, context=context) + + +class EDIMixin(object): + """Mixin class for Model objects that want be exposed as EDI documents. + Classes that inherit from this mixin class should override the + ``edi_import()`` and ``edi_export()`` methods to implement their + specific behavior, based on the primitives provided by this mixin.""" + + def _edi_requires_attributes(self, attributes, edi_document): + model_name = edi_document.get('__imported_model') or edi_document.get('__model') or self._name + for attribute in attributes: + assert edi_document.get(attribute),\ + 'Attribute `%s` is required in %s EDI documents' % (attribute, model_name) + + # private method, not RPC-exposed as it creates ir.model.data entries as + # SUPERUSER based on its parameters + def _edi_external_id(self, cr, uid, record, existing_id=None, existing_module=None, + context=None): + """Generate/Retrieve unique external ID for ``record``. + Each EDI record and each relationship attribute in it is identified by a + unique external ID, which includes the database's UUID, as a way to + refer to any record within any OpenERP instance, without conflict. + + For OpenERP records that have an existing "External ID" (i.e. an entry in + ir.model.data), the EDI unique identifier for this record will be made of + "%s:%s:%s" % (module, database UUID, ir.model.data ID). The database's + UUID MUST NOT contain a colon characters (this is guaranteed by the + UUID algorithm). + + For records that have no existing ir.model.data entry, a new one will be + created during the EDI export. It is recommended that the generated external ID + contains a readable reference to the record model, plus a unique value that + hides the database ID. If ``existing_id`` is provided (because it came from + an import), it will be used instead of generating a new one. + If ``existing_module`` is provided (because it came from + an import), it will be used instead of using local values. + + :param browse_record record: any browse_record needing an EDI external ID + :param string existing_id: optional existing external ID value, usually coming + from a just-imported EDI record, to be used instead + of generating a new one + :param string existing_module: optional existing module name, usually in the + format ``module:db_uuid`` and coming from a + just-imported EDI record, to be used instead + of local values + :return: the full unique External ID to use for record + """ + ir_model_data = self.pool.get('ir.model.data') + db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') + ext_id = record.get_external_id()[record.id] + if not ext_id: + ext_id = existing_id or safe_unique_id(db_uuid, record._name, record.id) + # ID is unique cross-db thanks to db_uuid (already included in existing_module) + module = existing_module or "%s:%s" % (record._original_module, db_uuid) + _logger.debug("%s: Generating new external ID `%s.%s` for %r", self._name, + module, ext_id, record) + ir_model_data.create(cr, openerp.SUPERUSER_ID, + {'name': ext_id, + 'model': record._name, + 'module': module, + 'res_id': record.id}) + else: + module, ext_id = ext_id.split('.') + if not ':' in module: + # this record was not previously EDI-imported + if not module == record._original_module: + # this could happen for data records defined in a module that depends + # on the module that owns the model, e.g. purchase defines + # product.pricelist records. + _logger.debug('Mismatching module: expected %s, got %s, for %s', + module, record._original_module, record) + # ID is unique cross-db thanks to db_uuid + module = "%s:%s" % (module, db_uuid) + + return '%s.%s' % (module, ext_id) + + def _edi_record_display_action(self, cr, uid, id, context=None): + """Returns an appropriate action definition dict for displaying + the record with ID ``rec_id``. + + :param int id: database ID of record to display + :return: action definition dict + """ + return {'type': 'ir.actions.act_window', + 'view_mode': 'form,tree', + 'view_type': 'form', + 'res_model': self._name, + 'res_id': id} + + def edi_metadata(self, cr, uid, records, context=None): + """Return a list containing the boilerplate EDI structures for + exporting ``records`` as EDI, including + the metadata fields + + The metadata fields always include:: + + { + '__model': 'some.model', # record model + '__module': 'module', # require module + '__id': 'module:db-uuid:model.id', # unique global external ID for the record + '__last_update': '2011-01-01 10:00:00', # last update date in UTC! + '__version': 1, # EDI spec version + '__generator' : 'OpenERP', # EDI generator + '__generator_version' : [6,1,0], # server version, to check compatibility. + '__attachments_': + } + + :param list(browse_record) records: records to export + :return: list of dicts containing boilerplate EDI metadata for each record, + at the corresponding index from ``records``. + """ + data_ids = [] + ir_attachment = self.pool.get('ir.attachment') + results = [] + for record in records: + ext_id = self._edi_external_id(cr, uid, record, context=context) + edi_dict = { + '__id': ext_id, + '__last_update': last_update_for(record), + '__model' : record._name, + '__module' : record._original_module, + '__version': EDI_PROTOCOL_VERSION, + '__generator': EDI_GENERATOR, + '__generator_version': EDI_GENERATOR_VERSION, + } + attachment_ids = ir_attachment.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)]) + if attachment_ids: + attachments = [] + for attachment in ir_attachment.browse(cr, uid, attachment_ids, context=context): + attachments.append({ + 'name' : attachment.name, + 'content': attachment.datas, # already base64 encoded! + 'file_name': attachment.datas_fname, + }) + edi_dict.update(__attachments=attachments) + results.append(edi_dict) + return results + + def edi_m2o(self, cr, uid, record, context=None): + """Return a m2o EDI representation for the given record. + + The EDI format for a many2one is:: + + ['unique_external_id', 'Document Name'] + """ + edi_ext_id = self._edi_external_id(cr, uid, record, context=context) + relation_model = record._model + name = relation_model.name_get(cr, uid, [record.id], context=context) + name = name and name[0][1] or False + return [edi_ext_id, name] + + def edi_o2m(self, cr, uid, records, edi_struct=None, context=None): + """Return a list representing a O2M EDI relationship containing + all the given records, according to the given ``edi_struct``. + This is basically the same as exporting all the record using + :meth:`~.edi_export` with the given ``edi_struct``, and wrapping + the results in a list. + + Example:: + + [ # O2M fields would be a list of dicts, with their + { '__id': 'module:db-uuid.id', # own __id. + '__last_update': 'iso date', # update date + 'name': 'some name', + #... + }, + # ... + ], + """ + result = [] + for record in records: + result += record._model.edi_export(cr, uid, [record], edi_struct=edi_struct, context=context) + return result + + def edi_m2m(self, cr, uid, records, context=None): + """Return a list representing a M2M EDI relationship directed towards + all the given records. + This is basically the same as exporting all the record using + :meth:`~.edi_m2o` and wrapping the results in a list. + + Example:: + + # M2M fields are exported as a list of pairs, like a list of M2O values + [ + ['module:db-uuid.id1', 'Task 01: bla bla'], + ['module:db-uuid.id2', 'Task 02: bla bla'] + ] + """ + return [self.edi_m2o(cr, uid, r, context=context) for r in records] + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + """Returns a list of dicts representing an edi.document containing the + records, and matching the given ``edi_struct``, if provided. + + :param edi_struct: if provided, edi_struct should be a dictionary + with a skeleton of the fields to export. + Basic fields can have any key as value, but o2m + values should have a sample skeleton dict as value, + to act like a recursive export. + For example, for a res.partner record:: + + edi_struct: { + 'name': True, + 'company_id': True, + 'address': { + 'name': True, + 'street': True, + } + } + + Any field not specified in the edi_struct will not + be included in the exported data. Fields with no + value (False) will be omitted in the EDI struct. + If edi_struct is omitted, no fields will be exported + """ + if edi_struct is None: + edi_struct = {} + fields_to_export = edi_struct.keys() + results = [] + for record in records: + edi_dict = self.edi_metadata(cr, uid, [record], context=context)[0] + for field in fields_to_export: + column = self._all_columns[field].column + value = getattr(record, field) + if not value and value not in ('', 0): + continue + elif column._type == 'many2one': + value = self.edi_m2o(cr, uid, value, context=context) + elif column._type == 'many2many': + value = self.edi_m2m(cr, uid, value, context=context) + elif column._type == 'one2many': + value = self.edi_o2m(cr, uid, value, edi_struct=edi_struct.get(field, {}), context=context) + edi_dict[field] = value + results.append(edi_dict) + return results + + def edi_export_and_email(self, cr, uid, ids, template_ext_id, context=None): + """Export the given records just like :meth:`~.export_edi`, the render the + given email template, in order to trigger appropriate notifications. + This method is intended to be called as part of business documents' + lifecycle, so it silently ignores any error occurring during the process, + as this is usually non-critical. To avoid any delay, it is also asynchronous + and will spawn a short-lived thread to perform the action. + + :param str template_ext_id: external id of the email.template to use for + the mail notifications + :return: True + """ + def email_task(): + db = pooler.get_db(cr.dbname) + local_cr = None + try: + time.sleep(3) # lame workaround to wait for commit of parent transaction + # grab a fresh browse_record on local cursor + local_cr = db.cursor() + web_root_url = self.pool.get('ir.config_parameter').get_param(local_cr, uid, 'web.base.url') + if not web_root_url: + _logger.warning('Ignoring EDI mail notification, web.base.url not defined in parameters') + return + mail_tmpl = self._edi_get_object_by_external_id(local_cr, uid, template_ext_id, 'email.template', context=context) + if not mail_tmpl: + # skip EDI export if the template was not found + _logger.warning('Ignoring EDI mail notification, template %s cannot be located', template_ext_id) + return + for edi_record in self.browse(local_cr, uid, ids, context=context): + edi_token = self.pool.get('edi.document').export_edi(local_cr, uid, [edi_record], context = context)[0] + edi_context = dict(context, edi_web_url_view=EDI_VIEW_WEB_URL % (web_root_url, local_cr.dbname, edi_token)) + self.pool.get('email.template').send_mail(local_cr, uid, mail_tmpl.id, edi_record.id, + force_send=False, context=edi_context) + _logger.info('EDI export successful for %s #%s, email notification sent.', self._name, edi_record.id) + except Exception: + _logger.warning('Ignoring EDI mail notification, failed to generate it.', exc_info=True) + finally: + if local_cr: + local_cr.commit() + local_cr.close() + + threading.Thread(target=email_task, name='EDI ExportAndEmail for %s %r' % (self._name, ids)).start() + return True + + def _edi_get_object_by_name(self, cr, uid, name, model_name, context=None): + model = self.pool.get(model_name) + search_results = model.name_search(cr, uid, name, operator='=', context=context) + if len(search_results) == 1: + return model.browse(cr, uid, search_results[0][0], context=context) + return False + + def _edi_generate_report_attachment(self, cr, uid, record, context=None): + """Utility method to generate the first PDF-type report declared for the + current model with ``usage`` attribute set to ``default``. + This must be called explicitly by models that need it, usually + at the beginning of ``edi_export``, before the call to ``super()``.""" + ir_actions_report = self.pool.get('ir.actions.report.xml') + matching_reports = ir_actions_report.search(cr, uid, [('model','=',self._name), + ('report_type','=','pdf'), + ('usage','=','default')]) + if matching_reports: + report = ir_actions_report.browse(cr, uid, matching_reports[0]) + report_service = 'report.' + report.report_name + service = netsvc.LocalService(report_service) + (result, format) = service.create(cr, uid, [record.id], {'model': self._name}, context=context) + eval_context = {'time': time, 'object': record} + if not report.attachment or not eval(report.attachment, eval_context): + # no auto-saving of report as attachment, need to do it manually + result = base64.b64encode(result) + file_name = record.name_get()[0][1] + file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name) + file_name += ".pdf" + ir_attachment = self.pool.get('ir.attachment').create(cr, uid, + {'name': file_name, + 'datas': result, + 'datas_fname': file_name, + 'res_model': self._name, + 'res_id': record.id}, + context=context) + + def _edi_import_attachments(self, cr, uid, record_id, edi_document, context=None): + ir_attachment = self.pool.get('ir.attachment') + for attachment in edi_document.get('__attachments', []): + # check attachment data is non-empty and valid + file_data = None + try: + file_data = base64.b64decode(attachment.get('content')) + except TypeError: + pass + assert file_data, 'Incorrect/Missing attachment file content' + assert attachment.get('name'), 'Incorrect/Missing attachment name' + assert attachment.get('file_name'), 'Incorrect/Missing attachment file name' + assert attachment.get('file_name'), 'Incorrect/Missing attachment file name' + ir_attachment.create(cr, uid, {'name': attachment['name'], + 'datas_fname': attachment['file_name'], + 'res_model': self._name, + 'res_id': record_id, + # should be pure 7bit ASCII + 'datas': str(attachment['content']), + }, context=context) + + + def _edi_get_object_by_external_id(self, cr, uid, external_id, model, context=None): + """Returns browse_record representing object identified by the model and external_id, + or None if no record was found with this external id. + + :param external_id: fully qualified external id, in the EDI form + ``module:db_uuid:identifier``. + :param model: model name the record belongs to. + """ + ir_model_data = self.pool.get('ir.model.data') + # external_id is expected to have the form: ``module:db_uuid:model.random_name`` + ext_id_members = split_external_id(external_id) + db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') + module = ext_id_members['module'] + ext_id = ext_id_members['id'] + modules = [] + ext_db_uuid = ext_id_members['db_uuid'] + if ext_db_uuid: + modules.append('%s:%s' % (module, ext_id_members['db_uuid'])) + if ext_db_uuid is None or ext_db_uuid == db_uuid: + # local records may also be registered without the db_uuid + modules.append(module) + data_ids = ir_model_data.search(cr, uid, [('model','=',model), + ('name','=',ext_id), + ('module','in',modules)]) + if data_ids: + model = self.pool.get(model) + data = ir_model_data.browse(cr, uid, data_ids[0], context=context) + result = model.browse(cr, uid, data.res_id, context=context) + return result + + def edi_import_relation(self, cr, uid, model, value, external_id, context=None): + """Imports a M2O/M2M relation EDI specification ``[external_id,value]`` for the + given model, returning the corresponding database ID: + + * First, checks if the ``external_id`` is already known, in which case the corresponding + database ID is directly returned, without doing anything else; + * If the ``external_id`` is unknown, attempts to locate an existing record + with the same ``value`` via name_search(). If found, the given external_id will + be assigned to this local record (in addition to any existing one) + * If previous steps gave no result, create a new record with the given + value in the target model, assign it the given external_id, and return + the new database ID + """ + _logger.debug("%s: Importing EDI relationship [%r,%r]", model, external_id, value) + target = self._edi_get_object_by_external_id(cr, uid, external_id, model, context=context) + need_new_ext_id = False + if not target: + _logger.debug("%s: Importing EDI relationship [%r,%r] - ID not found, trying name_get", + self._name, external_id, value) + target = self._edi_get_object_by_name(cr, uid, value, model, context=context) + need_new_ext_id = True + if not target: + _logger.debug("%s: Importing EDI relationship [%r,%r] - name not found, creating it!", + self._name, external_id, value) + # also need_new_ext_id here, but already been set above + model = self.pool.get(model) + res_id, name = model.name_create(cr, uid, value, context=context) + target = model.browse(cr, uid, res_id, context=context) + if need_new_ext_id: + ext_id_members = split_external_id(external_id) + # module name is never used bare when creating ir.model.data entries, in order + # to avoid being taken as part of the module's data, and cleanup up at next update + module = "%s:%s" % (ext_id_members['module'], ext_id_members['db_uuid']) + # create a new ir.model.data entry for this value + self._edi_external_id(cr, uid, target, existing_id=ext_id_members['id'], existing_module=module, context=context) + return target.id + + def edi_import(self, cr, uid, edi_document, context=None): + """Imports a dict representing an edi.document into the system. + + :param dict edi_document: EDI document to import + :return: the database ID of the imported record + """ + assert self._name == edi_document.get('__import_model') or \ + ('__import_model' not in edi_document and self._name == edi_document.get('__model')), \ + "EDI Document Model and current model do not match: '%s' (EDI) vs '%s' (current)" % \ + (edi_document['__model'], self._name) + + # First check the record is now already known in the database, in which case it is ignored + ext_id_members = split_external_id(edi_document['__id']) + existing = self._edi_get_object_by_external_id(cr, uid, ext_id_members['full'], self._name, context=context) + if existing: + _logger.info("'%s' EDI Document with ID '%s' is already known, skipping import!", self._name, ext_id_members['full']) + return existing.id + + record_values = {} + o2m_todo = {} # o2m values are processed after their parent already exists + for field_name, field_value in edi_document.iteritems(): + # skip metadata and empty fields + if field_name.startswith('__') or field_value is None or field_value is False: + continue + field_info = self._all_columns.get(field_name) + if not field_info: + _logger.warning('Ignoring unknown field `%s` when importing `%s` EDI document', field_name, self._name) + continue + field = field_info.column + # skip function/related fields + if isinstance(field, fields.function): + _logger.warning("Unexpected function field value found in '%s' EDI document: '%s'" % (self._name, field_name)) + continue + relation_model = field._obj + if field._type == 'many2one': + record_values[field_name] = self.edi_import_relation(cr, uid, relation_model, + field_value[1], field_value[0], + context=context) + elif field._type == 'many2many': + record_values[field_name] = [self.edi_import_relation(cr, uid, relation_model, m2m_value[1], + m2m_value[0], context=context) + for m2m_value in field_value] + elif field._type == 'one2many': + # must wait until parent report is imported, as the parent relationship + # is often required in o2m child records + o2m_todo[field_name] = field_value + else: + record_values[field_name] = field_value + + module_ref = "%s:%s" % (ext_id_members['module'], ext_id_members['db_uuid']) + record_id = self.pool.get('ir.model.data')._update(cr, uid, self._name, module_ref, record_values, + xml_id=ext_id_members['id'], context=context) + + record_display, = self.name_get(cr, uid, [record_id], context=context) + + # process o2m values, connecting them to their parent on-the-fly + for o2m_field, o2m_value in o2m_todo.iteritems(): + field = self._all_columns[o2m_field].column + dest_model = self.pool.get(field._obj) + for o2m_line in o2m_value: + # link to parent record: expects an (ext_id, name) pair + o2m_line[field._fields_id] = (ext_id_members['full'], record_display[1]) + dest_model.edi_import(cr, uid, o2m_line, context=context) + + # process the attachments, if any + self._edi_import_attachments(cr, uid, record_id, edi_document, context=context) + + return record_id + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/edi/models/res_company.py b/addons/edi/models/res_company.py new file mode 100644 index 00000000000..665dfb9487d --- /dev/null +++ b/addons/edi/models/res_company.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import fields,osv + +class res_company(osv.osv): + """Helper subclass for res.company providing util methods for working with + companies in the context of EDI import/export. The res.company object + itself is not EDI-exportable""" + _inherit = "res.company" + + def edi_export_address(self, cr, uid, company, edi_address_struct=None, context=None): + """Returns a dict representation of the address of the company record, suitable for + inclusion in an EDI document, and matching the given edi_address_struct if provided. + The first found address is returned, in order of preference: invoice, contact, default. + + :param browse_record company: company to export + :return: dict containing the address representation for the company record, or + an empty dict if no address can be found + """ + res_partner = self.pool.get('res.partner') + res_partner_address = self.pool.get('res.partner.address') + addresses = res_partner.address_get(cr, uid, [company.partner_id.id], ['default', 'contact', 'invoice']) + addr_id = addresses['invoice'] or addresses['contact'] or addresses['default'] + result = {} + if addr_id: + address = res_partner_address.browse(cr, uid, addr_id, context=context) + result = res_partner_address.edi_export(cr, uid, [address], edi_struct=edi_address_struct, context=context)[0] + if company.logo: + result['logo'] = company.logo # already base64-encoded + if company.paypal_account: + result['paypal_account'] = company.paypal_account + # bank info: include only bank account supposed to be displayed in document footers + res_partner_bank = self.pool.get('res.partner.bank') + bank_ids = res_partner_bank.search(cr, uid, [('company_id','=',company.id),('footer','=',True)], context=context) + if bank_ids: + result['bank_ids'] = res_partner_address.edi_m2m(cr, uid, + res_partner_bank.browse(cr, uid, bank_ids, context=context), + context=context) + return result diff --git a/addons/edi/models/res_currency.py b/addons/edi/models/res_currency.py new file mode 100644 index 00000000000..d057f4e5bbc --- /dev/null +++ b/addons/edi/models/res_currency.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import fields,osv +from edi import EDIMixin +from openerp import SUPERUSER_ID + +RES_CURRENCY_EDI_STRUCT = { + #custom: 'code' + 'symbol': True, + 'rate': True, +} + +class res_currency(osv.osv, EDIMixin): + _inherit = "res.currency" + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + edi_struct = dict(edi_struct or RES_CURRENCY_EDI_STRUCT) + edi_doc_list = [] + for currency in records: + # Get EDI doc based on struct. The result will also contain all metadata fields and attachments. + edi_doc = super(res_currency,self).edi_export(cr, uid, [currency], edi_struct, context)[0] + edi_doc.update(code=currency.name) + edi_doc_list.append(edi_doc) + return edi_doc_list + + def edi_import(self, cr, uid, edi_document, context=None): + self._edi_requires_attributes(('code','symbol'), edi_document) + external_id = edi_document['__id'] + existing_currency = self._edi_get_object_by_external_id(cr, uid, external_id, 'res_currency', context=context) + if existing_currency: + return existing_currency.id + + # find with unique ISO code + existing_ids = self.search(cr, uid, [('name','=',edi_document['code'])]) + if existing_ids: + return existing_ids[0] + + # nothing found, create a new one + currency_id = self.create(cr, SUPERUSER_ID, {'name': edi_document['code'], + 'symbol': edi_document['symbol']}, context=context) + rate = edi_document.pop('rate') + if rate: + self.pool.get('res.currency.rate').create(cr, SUPERUSER_ID, {'currency_id': currency_id, + 'rate': rate}, context=context) + return currency_id \ No newline at end of file diff --git a/addons/edi/models/res_partner.py b/addons/edi/models/res_partner.py new file mode 100644 index 00000000000..18ec01eb765 --- /dev/null +++ b/addons/edi/models/res_partner.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import logging + +from osv import fields,osv +from edi import EDIMixin +from openerp import SUPERUSER_ID +from tools.translate import _ + +RES_PARTNER_ADDRESS_EDI_STRUCT = { + 'name': True, + 'email': True, + 'street': True, + 'street2': True, + 'zip': True, + 'city': True, + 'country_id': True, + 'state_id': True, + 'phone': True, + 'fax': True, + 'mobile': True, +} + +RES_PARTNER_EDI_STRUCT = { + 'name': True, + 'ref': True, + 'lang': True, + 'website': True, + 'address': RES_PARTNER_ADDRESS_EDI_STRUCT +} + +class res_partner(osv.osv, EDIMixin): + _inherit = "res.partner" + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + return super(res_partner,self).edi_export(cr, uid, records, + edi_struct or dict(RES_PARTNER_EDI_STRUCT), + context=context) + +class res_partner_address(osv.osv, EDIMixin): + _inherit = "res.partner.address" + + def _get_bank_type(self, cr, uid, context=None): + # first option: the "normal" bank type, installed by default + res_partner_bank_type = self.pool.get('res.partner.bank.type') + try: + return self.pool.get('ir.model.data').get_object(cr, uid, 'base', 'bank_normal', context=context).code + except ValueError: + pass + + # second option: create a new custom type for EDI or use it if already created, as IBAN type is + # not always appropriate: we need a free-form bank type for max flexibility (users can correct + # data manually after import) + code, label = 'edi_generic', 'Generic Bank Type (auto-created for EDI)' + bank_code_ids = res_partner_bank_type.search(cr, uid, [('code','=',code)], context=context) + if not bank_code_ids: + logging.getLogger('edi.res_partner').info('Normal bank account type is missing, creating ' + 'a generic bank account type for EDI.') + self.res_partner_bank_type.create(cr, SUPERUSER_ID, {'name': label, + 'code': label}) + return code + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + return super(res_partner_address,self).edi_export(cr, uid, records, + edi_struct or dict(RES_PARTNER_ADDRESS_EDI_STRUCT), + context=context) + + def edi_import(self, cr, uid, edi_document, context=None): + # handle bank info, if any + edi_bank_ids = edi_document.pop('bank_ids', None) + address_id = super(res_partner_address,self).edi_import(cr, uid, edi_document, context=context) + if edi_bank_ids: + address = self.browse(cr, uid, address_id, context=context) + import_ctx = dict((context or {}), + default_partner_id=address.partner_id.id, + default_state=self._get_bank_type(cr, uid, context)) + for ext_bank_id, bank_name in edi_bank_ids: + try: + self.edi_import_relation(cr, uid, 'res.partner.bank', + bank_name, ext_bank_id, context=import_ctx) + except osv.except_osv: + # failed to import it, try again with unrestricted default type + logging.getLogger('edi.res_partner').warning('Failed to import bank account using' + 'bank type: %s, ignoring', import_ctx['default_state'], + exc_info=True) + return address_id \ No newline at end of file diff --git a/addons/edi/security/ir.model.access.csv b/addons/edi/security/ir.model.access.csv new file mode 100644 index 00000000000..f1942397ac5 --- /dev/null +++ b/addons/edi/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_ir_edi_all_read","access_ir_edi_all_read","model_edi_document",,1,0,0,0 +"access_ir_edi_employee_create","access_ir_edi_employee_create","model_edi_document","base.group_user",1,0,1,0 \ No newline at end of file diff --git a/addons/edi/static/src/css/edi.css b/addons/edi/static/src/css/edi.css new file mode 100644 index 00000000000..478985ee585 --- /dev/null +++ b/addons/edi/static/src/css/edi.css @@ -0,0 +1,220 @@ +/** EDI content **/ +.openerp .company_logo { + background-size: 180px 46px; +} +.oe_edi_view { + width: 65%; + vertical-align: top; + padding: 0px 25px; + border-right: 1px solid #D2CFCF; +} +.oe_edi_sidebar_container { + width: 35%; + padding: 0px 10px; + vertical-align: top; +} +button.oe_edi_action_print { + font-size: 1.5em; + margin-left: 35%; + margin-bottom: 20px; +} +button.oe_edi_action_print img { + vertical-align: bottom; + width: 32px; + height: 32px; +} + +.openerp input.invalid { + background-color: #F66 !important; + border: 1px solid #D00 !important; + color: #000; +} +/* following browse-specific hacks need to be separate rules, as browsers are + required to ignore rules with unknown selectors. At time of writing, there + is no standard way to style HTML5 placeholders */ +.openerp input.invalid:-moz-placeholder { + color: #FFF; +} +.openerp input.invalid::-webkit-input-placeholder { + color: #FFF; +} + +/** EDI Sidebar **/ +.oe_edi_sidebar_title { + border-bottom: 1px solid #D2CFCF; + font-weight: bold; + font-size: 1.3em; + min-width: 10em; +} +.oe_edi_nested_block, .oe_edi_nested_block_import, .oe_edi_nested_block_pay { + margin: 0px 40px; + min-width: 10em; + display: none; /* made visible by click on parent input/label */ +} +.oe_edi_right_top .oe_edi_nested_block label { + float: left; + text-align: right; + margin-right: 0.5em; + line-height: 180%; + font-weight: bold; + min-width: 5em; +} +.oe_edi_option { + padding-left: 5px; + line-height: 2em; +} +.oe_edi_option:hover { + background: #e8e8e8; +} +.oe_edi_import_button { + margin: 2px 10px; + white-space: nowrap; +} +.oe_edi_small, .oe_edi_small input { + font-size: 90%; +} + +/** Sidebar bottom **/ +.oe_edi_paypal_button { + margin: 6px; +} + + +/** Paperbox, from http://www.sitepoint.com/pure-css3-paper-curl/ **/ +body { + background: #EEE; /* contrast with paper */ +} +.oe_edi_paperbox { + position: relative; + width: 700px; + padding: 30px; + padding-bottom: 50px; + margin: 20px auto; + background-color: #fff; + -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); +} +.oe_edi_paperbox:before, .oe_edi_paperbox:after { + position: absolute; + width: 40%; + height: 10px; + content: ' '; + left: 12px; + bottom: 15px; + background: transparent; + -webkit-transform: skew(-5deg) rotate(-5deg); + -moz-transform: skew(-5deg) rotate(-5deg); + -ms-transform: skew(-5deg) rotate(-5deg); + -o-transform: skew(-5deg) rotate(-5deg); + transform: skew(-5deg) rotate(-5deg); + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + z-index: -1; +} +.oe_edi_paperbox:after { + left: auto; right: 12px; + -webkit-transform: skew(5deg) rotate(5deg); + -moz-transform: skew(5deg) rotate(5deg); + -ms-transform: skew(5deg) rotate(5deg); + -o-transform: skew(5deg) rotate(5deg); + transform: skew(5deg) rotate(5deg); +} + +/** Sale Order / Purchase Order Preview **/ +table.oe_edi_data, .oe_edi_doc_title { + border-collapse: collapse; + clear: both; +} +.oe_edi_data th { + white-space: nowrap; +} +.oe_edi_data .oe_edi_floor { + border-bottom: 1px solid black; +} +.oe_edi_data .oe_edi_ceiling { + border-top: 1px solid black; +} +.oe_edi_data .oe_edi_data_row { + border-bottom: 1px solid #D2CFCF; +} +.oe_edi_data_row td { + vertical-align: top; +} +.oe_edi_inner_note { + font-style: italic; + font-size: 95%; + padding-left: 10px; +} +.oe_edi_shade { + background: #e8e8e8; +} +.oe_edi_company_name { + text-transform: uppercase; + font-weight: bold; +} +.oe_edi_address_from { + float: left; +} +.oe_edi_address_to { + float: right; + margin-top: 25px; + margin-bottom: 30px; +} +.oe_edi_company_block_title { + width: 375px; + margin: 0px; + padding: 2px 14px; + background-color: #252525; + border-top-left-radius: 5px 5px; + border-top-right-radius: 5px 5px; + background-repeat: repeat no-repeat; +} +.oe_edi_company_block_title .oe_edi_company_name { + margin: 0px; + font-size: 1em; + color: #FFF; +} +.oe_edi_company_block_body { + width: 375px; + margin: 0px; + padding: 5px 14px; + line-height: 16px; + background-color: rgb(242, 242, 242); +} +.oe_edi_company_block_body p { + color: #222; + margin: 5px 0px; +} +.oe_edi_summary_label { + float: left; +} +.oe_edi_summary_value { + float: right; +} + + +/** Python code highlighting **/ +/* GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2008 Benny Baumann + (http://qbnz.com/highlighter/ and http://geshi.org/) */ +.python .de1, .python .de2 {font: normal normal 1em/1.2em monospace; margin:0; padding:0; background:none; vertical-align:top;} +.python {font-family:monospace;} +.python .imp {font-weight: bold; color: red;} +.python li, .python .li1 {background: #ffffff; list-style: none;} +.python .ln {width:1px;text-align:right;margin:0;padding:0 2px;vertical-align:top;} +.python .li2 {background: #f8f8f8;} +.python .kw1 {color: #ff7700;font-weight:bold;} +.python .kw2 {color: #008000;} +.python .kw3 {color: #dc143c;} +.python .kw4 {color: #0000cd;} +.python .co1 {color: #808080; font-style: italic;} +.python .coMULTI {color: #808080; font-style: italic;} +.python .es0 {color: #000099; font-weight: bold;} +.python .br0 {color: black;} +.python .sy0 {color: #66cc66;} +.python .st0 {color: #483d8b;} +.python .nu0 {color: #ff4500;} +.python .me1 {color: black;} +.python span.xtra { display:block; } +.python ol { padding: 0px; } \ No newline at end of file diff --git a/addons/edi/static/src/img/pdf.png b/addons/edi/static/src/img/pdf.png new file mode 100644 index 00000000000..d35dfd20c2d Binary files /dev/null and b/addons/edi/static/src/img/pdf.png differ diff --git a/addons/edi/static/src/js/edi.js b/addons/edi/static/src/js/edi.js new file mode 100644 index 00000000000..7eb118baeb3 --- /dev/null +++ b/addons/edi/static/src/js/edi.js @@ -0,0 +1,175 @@ +openerp.edi = function(openerp) { +openerp.web.qweb.add_template("/web/static/src/xml/base.xml"); +openerp.web.qweb.add_template("/edi/static/src/xml/edi.xml"); +openerp.web.qweb.add_template("/edi/static/src/xml/edi_account.xml"); +openerp.web.qweb.add_template("/edi/static/src/xml/edi_sale_purchase.xml"); +openerp.edi = {} + +openerp.edi.EdiView = openerp.web.Widget.extend({ + init: function(parent, db, token) { + this._super(); + this.db = db; + this.token = token; + this.session = new openerp.web.Connection(); + this.template = "EdiEmpty"; + this.content = ""; + this.sidebar = ""; + }, + start: function() { + this._super(); + var param = {"db": this.db, "token": this.token}; + this.rpc('/edi/get_edi_document', param, this.on_document_loaded, this.on_document_failed); + }, + on_document_loaded: function(docs){ + this.doc = docs[0]; + var template_content = "Edi." + this.doc.__model + ".content"; + var template_sidebar = "Edi." + this.doc.__model + ".sidebar"; + var param = {"widget":this, "doc":this.doc}; + if (openerp.web.qweb.templates[template_sidebar]) { + this.sidebar = openerp.web.qweb.render(template_sidebar, param); + } + if (openerp.web.qweb.templates[template_content]) { + this.content = openerp.web.qweb.render(template_content, param); + } + this.$element.html(openerp.web.qweb.render("EdiView", param)); + this.$element.find('button.oe_edi_action_print').bind('click', this.do_print); + this.$element.find('button#oe_edi_import_existing').bind('click', this.do_import_existing); + this.$element.find('button#oe_edi_import_create').bind('click', this.do_import_create); + this.$element.find('button#oe_edi_download').bind('click', this.do_download); + this.$element.find('.oe_edi_import_choice, .oe_edi_import_choice_label').bind('click', this.toggle_choice('import')); + this.$element.find('.oe_edi_pay_choice, .oe_edi_pay_choice_label').bind('click', this.toggle_choice('pay')); + this.$element.find('#oe_edi_download_show_code').bind('click', this.show_code); + }, + on_document_failed: function(response) { + var self = this; + var params = { + error: response, + //TODO: should this be _t() wrapped? + message: "Sorry, this document cannot be located. Perhaps the link you are using has expired?" + } + $(openerp.web.qweb.render("DialogWarning", params)).dialog({ + title: "Document not found", + modal: true, + }); + }, + show_code: function($event) { + $('#oe_edi_download_code').toggle(); + }, + get_download_url: function() { + var l = window.location; + var url_prefix = l.protocol + '//' + l.host; + return url_prefix +'/edi/download?db=' + this.db + '&token=' + this.token; + }, + get_paypal_url: function(document_type, ref_field) { + var comp_name = encodeURIComponent(this.doc.company_id[1]); + var doc_ref = encodeURIComponent(this.doc[ref_field]); + var paypal_account = encodeURIComponent(this.doc.company_address.paypal_account); + var amount = encodeURIComponent(this.doc.amount_total); + var cur_code = encodeURIComponent(this.doc.currency.code); + var paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick" + + "&business=" + paypal_account + + "&item_name=" + document_type + "%20" + comp_name + "%20" + doc_ref + + "&invoice=" + doc_ref + + "&amount=" + amount + + "¤cy_code=" + cur_code + + "&button_subtype=services&no_note=1&bn=OpenERP_PayNow_" + cur_code; + return paypal_url; + }, + toggle_choice: function(mode) { + return function($e) { + $('.oe_edi_nested_block_'+mode).hide(); + $('.'+$e.target.id+'_nested').show(); + return true; + } + }, + do_print: function(e){ + var l = window.location; + window.location = l.protocol + '//' + l.host + "/edi/download_attachment?db=" + this.db + "&token=" + this.token; + }, + do_import_existing: function(e) { + var url_download = this.get_download_url(); + var $edi_text_server_input = this.$element.find('#oe_edi_txt_server_url'); + var server_url = $edi_text_server_input.val(); + $edi_text_server_input.removeClass('invalid'); + if (!server_url) { + $edi_text_server_input.addClass('invalid'); + return false; + } + var protocol = "http://"; + if (server_url.toLowerCase().lastIndexOf('http', 0) == 0 ) { + protocol = ''; + } + window.location = protocol + server_url + '/edi/import_url?url=' + encodeURIComponent(url_download); + }, + do_import_create: function(e){ + var url_download = this.get_download_url(); + window.location = "https://cc.my.openerp.com/odms/create_edi?url=" + encodeURIComponent(url_download); + }, + do_download: function(e){ + window.location = this.get_download_url(); + } +}); + +openerp.edi.EdiImport = openerp.web.Widget.extend({ + init: function(parent,url) { + this._super(); + this.url = url; + var params = {}; + + this.template = "EdiImport"; + this.session = new openerp.web.Connection(); + this.login = new openerp.web.Login(this); + this.header = new openerp.web.Header(this); + this.header.on_logout.add(this.login.on_logout); + }, + start: function() { + this.session.on_session_invalid.add_last(this.do_ask_login) + this.session.on_session_valid.add_last(this.do_import); + this.header.appendTo($("#oe_header")); + this.session.start(); + this.login.appendTo($('#oe_login')); + }, + do_ask_login: function() { + this.login.do_ask_login(this.do_import); + }, + do_import: function() { + this.rpc('/edi/import_edi_url', {url: this.url}, this.on_imported, this.on_imported_error); + }, + on_imported: function(response) { + if ('action' in response) { + this.rpc("/web/session/save_session_action", {the_action: response.action}, function(key) { + window.location = "/web/webclient/home?debug=1&s_action="+encodeURIComponent(key); + }); + } + else { + $('
').dialog({ + modal: true, + title: 'Import Successful!', + buttons: { + Ok: function() { + $(this).dialog("close"); + window.location = "/web/webclient/home"; + } + } + }).html('The document has been successfully imported!'); + } + }, + on_imported_error: function(response){ + var self = this; + var msg = "Sorry, the document could not be imported."; + if (response.data.fault_code) { + msg += "\n Reason:" + response.data.fault_code; + } + var params = {error: response, message: msg}; + $(openerp.web.qweb.render("DialogWarning", params)).dialog({ + title: "Document Import Notification", + modal: true, + buttons: { + Ok: function() { $(this).dialog("close"); } + } + }); + } +}); + +} +// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/edi/static/src/xml/edi.xml b/addons/edi/static/src/xml/edi.xml new file mode 100644 index 00000000000..1e662a2b2cc --- /dev/null +++ b/addons/edi/static/src/xml/edi.xml @@ -0,0 +1,93 @@ + diff --git a/addons/edi/static/src/xml/edi_account.xml b/addons/edi/static/src/xml/edi_account.xml new file mode 100644 index 00000000000..d9c9af9c401 --- /dev/null +++ b/addons/edi/static/src/xml/edi_account.xml @@ -0,0 +1,163 @@ + diff --git a/addons/edi/static/src/xml/edi_sale_purchase.xml b/addons/edi/static/src/xml/edi_sale_purchase.xml new file mode 100644 index 00000000000..cbd16507b62 --- /dev/null +++ b/addons/edi/static/src/xml/edi_sale_purchase.xml @@ -0,0 +1,161 @@ + diff --git a/addons/edi/test/edi_partner_test.yml b/addons/edi/test/edi_partner_test.yml new file mode 100644 index 00000000000..cca39417927 --- /dev/null +++ b/addons/edi/test/edi_partner_test.yml @@ -0,0 +1,44 @@ +- + In order to test the basic EDI system, I will export a partner, + modify the exported EDI document to add an attachment and change + the data, and then re-import it and re-export it. + + with an attached file, check the result, the alter the data + and reimport it. +- + !python {model: edi.document}: | + import json + partner_obj = self.pool.get('res.partner') + tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, ref('base.res_partner_agrolait'))]) + doc = self.get_document(cr, uid, tokens[0], context=context) + edi_doc, = json.loads(doc) + + # check content of the document + assert edi_doc.get('__id').endswith('.res_partner_agrolait'), 'Incorrect external ID' + assert edi_doc.get('__model') == 'res.partner', 'Incorrect/Missing __model' + assert edi_doc.get('__module') == 'base', 'Incorrect/Missing __module' + assert edi_doc.get('__last_update'), 'Missing __last_update' + + # try to import the document after changing the name and id, and attaching a + # file, and check that a new partner is returned + edi_doc['__id'] = 'base:xxd37f8a-xx55-11e0-xxdd-xx81124c8b50.res_partner_xxx' + edi_doc['name'] = 'AgroMilk' + attachment = { + 'name': 'Test file', + 'file_name': 'test.png', + # base64 standard requires blocks of 57bytes=76chars, NL-separated + 'content': 'iVBORw0KGgoAAAANSUhEUgAAAA4AAAAKCAYAAACE2W/HAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9oIDQ84BkjLWAYAAACNSURBVCjP\njZKxDUJBDEOf/wbUbEHDhkxAzRjUDECBhMQYjIApuPBNpK+PpUinOPYll5Nt1iCJXifbSPpJZtHg\nXLUdu0FWpMEt87a/xtsmqjjNetPFyhuWYFuj7RcgQBNwXNGdw2CqY85yXWi5z7YBnmRyEHvg0YSX\nrLE9P30nwugOHHr+s5va4x+fofAGm1+JjnJICm0AAAAASUVORK5CYII=\n', + } + edi_doc['__attachments'] = [attachment] + doc = json.dumps([edi_doc]) + result, = self.import_edi(cr, uid, edi_document=doc) + assert result[0] == 'res.partner' and result[1] > ref('base.res_partner_agrolait'),\ + "Expected (%r,> %r) after import 1, got %r" % ('res.partner', ref('base.res_partner_agrolait'), result) + + # export the same partner we just created, and see if the output matches the input + tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, result[1])]) + doc_output = self.get_document(cr, uid, tokens[0], context=context) + edi_doc_output, = json.loads(doc_output) + for attribute in ('__model', '__module', '__id', 'name', '__attachments'): + assert edi_doc_output.get(attribute) == edi_doc.get(attribute), \ + 'Incorrect value for %s, expected %r, got %r' % (attribute, edi_doc.get(attribute), edi_doc_output.get(attribute)) \ No newline at end of file diff --git a/addons/email_template/__init__.py b/addons/email_template/__init__.py index 97931ea8bd5..d1cf5b28431 100644 --- a/addons/email_template/__init__.py +++ b/addons/email_template/__init__.py @@ -21,5 +21,6 @@ ############################################################################## import email_template import wizard +import res_partner # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py index 06fa969c0c2..2ab7f194d18 100644 --- a/addons/email_template/__openerp__.py +++ b/addons/email_template/__openerp__.py @@ -61,9 +61,13 @@ Openlabs was kept "data": [ 'wizard/email_template_preview_view.xml', 'email_template_view.xml', + 'res_partner_view.xml', 'wizard/email_compose_message_view.xml', 'security/ir.model.access.csv' ], + "demo": [ + 'res_partner_demo.yml', + ], "installable": True, "active": False, "certificate" : "00817073628967384349", diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 3917b3f2ddb..237279c16aa 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -28,6 +28,7 @@ from osv import osv from osv import fields import tools from tools.translate import _ +from urllib import quote as quote try: from mako.template import Template as MakoTemplate @@ -66,6 +67,7 @@ class email_template(osv.osv): user=user, # context kw would clash with mako internals ctx=context, + quote=quote, format_exceptions=True) if result == u'False': result = u'' @@ -305,6 +307,7 @@ class email_template(osv.osv): 'attachment_ids': False, 'message_id': False, 'state': 'outgoing', + 'subtype': 'plain', } if not template_id: return values @@ -319,6 +322,9 @@ class email_template(osv.osv): template.model, res_id, context=context) \ or False + if values['body_html']: + values.update(subtype='html') + if template.user_signature: signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature values['body_text'] += '\n\n' + signature @@ -355,20 +361,24 @@ class email_template(osv.osv): values['attachments'] = attachments return values - def send_mail(self, cr, uid, template_id, res_id, context=None): + def send_mail(self, cr, uid, template_id, res_id, force_send=False, context=None): """Generates a new mail message for the given template and record, - and schedule it for delivery through the ``mail`` module's scheduler. + and schedules it for delivery through the ``mail`` module's scheduler. :param int template_id: id of the template to render :param int res_id: id of the record to render the template with (model is taken from the template) + :param bool force_send: if True, the generated mail.message is + immediately sent after being created, as if the scheduler + was executed for this message only. + :returns: id of the mail.message that was created """ mail_message = self.pool.get('mail.message') ir_attachment = self.pool.get('ir.attachment') template = self.browse(cr, uid, template_id, context) values = self.generate_email(cr, uid, template_id, res_id, context=context) attachments = values.pop('attachments') or {} - message_id = mail_message.create(cr, uid, values, context=context) + msg_id = mail_message.create(cr, uid, values, context=context) # link attachments attachment_ids = [] for fname, fcontent in attachments.iteritems(): @@ -377,10 +387,13 @@ class email_template(osv.osv): 'datas_fname': fname, 'datas': fcontent, 'res_model': mail_message._name, - 'res_id': message_id, + 'res_id': msg_id, } if context.has_key('default_type'): del context['default_type'] - attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context)) + attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context=context)) + if force_send: + mail_message.send(cr, uid, [msg_id], context=context) + return msg_id # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/email_template_view.xml b/addons/email_template/email_template_view.xml index d6a651f0bc2..507dae3cd25 100644 --- a/addons/email_template/email_template_view.xml +++ b/addons/email_template/email_template_view.xml @@ -155,9 +155,8 @@ - - + diff --git a/addons/email_template/res_partner.py b/addons/email_template/res_partner.py new file mode 100644 index 00000000000..74d8b4e0693 --- /dev/null +++ b/addons/email_template/res_partner.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import fields,osv + +class res_partner(osv.osv): + """Inherit res.partner to add a generic opt-out field that can be used + to restrict usage of automatic email templates. + This field is unused by default. """ + _inherit = 'res.partner' + + _columns = { + 'opt_out': fields.boolean('Opt-out', help="If checked, this partner will not receive any automated email " \ + "notifications, such as the availability of invoices."), + } + + _defaults = { + 'opt_out': False, + } \ No newline at end of file diff --git a/addons/email_template/res_partner_demo.yml b/addons/email_template/res_partner_demo.yml new file mode 100644 index 00000000000..8eaf48f9e87 --- /dev/null +++ b/addons/email_template/res_partner_demo.yml @@ -0,0 +1,9 @@ +- + Set opt-out to True on all demo partners +- + !python {model: res.partner}: | + partner_ids = self.search(cr, uid, []) + # assume partners with an external ID come from demo data + ext_ids = self._get_external_ids(cr, uid, partner_ids) + ids_to_update = [k for (k,v) in ext_ids.iteritems() if v] + self.write(cr, uid, ids_to_update, {'opt_out': True}) \ No newline at end of file diff --git a/addons/email_template/res_partner_view.xml b/addons/email_template/res_partner_view.xml new file mode 100644 index 00000000000..0d2f919a96e --- /dev/null +++ b/addons/email_template/res_partner_view.xml @@ -0,0 +1,16 @@ + + + + + res.partner.opt_out.form + res.partner + form + + + + + + + + + \ No newline at end of file diff --git a/addons/email_template/wizard/email_template_preview.py b/addons/email_template/wizard/email_template_preview.py index 650b714866d..4ef0e9fa156 100644 --- a/addons/email_template/wizard/email_template_preview.py +++ b/addons/email_template/wizard/email_template_preview.py @@ -87,6 +87,8 @@ class email_template_preview(osv.osv_memory): signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature description += '\n' + signature vals['body_text'] = description + if template.body_html: + vals['body_html'] = self.render_template(cr, uid, template.body_html, model, res_id, context) or '' vals['report_name'] = self.render_template(cr, uid, template.report_name, model, res_id, context) return {'value': vals} diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 9c535966e17..159259f151d 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -479,11 +479,22 @@ class mail_message(osv.osv): attachments = [] for attach in message.attachment_ids: attachments.append((attach.datas_fname, base64.b64decode(attach.datas))) + + body = message.body_html if message.subtype == 'html' else message.body_text + body_alternative = None + subtype_alternative = None + if message.subtype == 'html' and message.body_text: + # we have a plain text alternative prepared, pass it to + # build_message instead of letting it build one + body_alternative = message.body_text + subtype_alternative = 'plain' + msg = ir_mail_server.build_email( email_from=message.email_from, email_to=to_email(message.email_to), subject=message.subject, - body=message.body_html if message.subtype == 'html' else message.body_text, + body=body, + body_alternative=body_alternative, email_cc=to_email(message.email_cc), email_bcc=to_email(message.email_bcc), reply_to=message.reply_to, @@ -491,6 +502,7 @@ class mail_message(osv.osv): references = message.references, object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)), subtype=message.subtype, + subtype_alternative=subtype_alternative, headers=message.headers and literal_eval(message.headers)) res = ir_mail_server.send_email(cr, uid, msg, mail_server_id=message.mail_server_id.id, diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml index 96268435810..3169bca76c2 100644 --- a/addons/mail/mail_message_view.xml +++ b/addons/mail/mail_message_view.xml @@ -1,9 +1,6 @@ - - - mail.message.form mail.message @@ -144,14 +141,10 @@ src_model="res.partner" view_id="view_email_message_tree"/> - - - - diff --git a/addons/membership/test/test_membership.yml b/addons/membership/test/test_membership.yml index 75e0f887cfe..27b755f156b 100644 --- a/addons/membership/test/test_membership.yml +++ b/addons/membership/test/test_membership.yml @@ -13,36 +13,22 @@ list_price: 80.00 - | - "Mark Johnson" want to join "Gold Membership". -- | - I'm creating new member "Mark Johnson". + "Seagate" want to join "Gold Membership", so I'm checking "Current Membership State" of "Seagate". It is an "Non Member" or not. - - !record {model: res.partner, id: res_partner_markjohnson0}: - address: - - city: paris - country_id: base.fr - name: Mark Johnson - street: 1 rue Rockfeller - type: invoice - zip: '75016' - name: Mark Johnson -- | - I'm checking "Current Membership State" of "Mark Johnson". It is an "Non Member" or not. -- - !assert {model: res.partner, id: res_partner_markjohnson0}: + !assert {model: res.partner, id: base.res_partner_seagate}: - membership_state == 'none', 'Member should be has "Current Membership State" in "Non Member".' - | - I'm doing to make membership invoice for "Mark Johnson" on joining "Gold Membership". + I'm doing to make membership invoice for "Seagate" on joining "Gold Membership". - !python {model: res.partner}: | - self.create_membership_invoice(cr, uid, [ref("res_partner_markjohnson0")], product_id=ref("product_product_membershipproduct0"), datas={"amount":80.00}) + self.create_membership_invoice(cr, uid, [ref("base.res_partner_seagate")], product_id=ref("product_product_membershipproduct0"), datas={"amount":80.00}) - | - I'm checking "Current Membership State" of "Mark Johnson". It is an "Waiting Member" or not. + I'm checking "Current Membership State" of "Seagate". It is an "Waiting Member" or not. - - !assert {model: res.partner, id: res_partner_markjohnson0}: + !assert {model: res.partner, id: base.res_partner_seagate}: - membership_state == 'waiting', 'Member should be has "Current Membership State" in "Waiting Member".' - | - I'm Opening that Invoice which is created for "Mark Johnson". + I'm Opening that Invoice which is created for "Seagate". - !python {model: res.partner}: | import netsvc @@ -52,16 +38,16 @@ membership_line_pool = self.pool.get('membership.membership_line') membership_pool = self.pool.get('product.product') - membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('res_partner_markjohnson0'))]) + membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('base.res_partner_seagate'))]) membership_lines = membership_line_pool.browse(cr, uid, membership_line_ids) assert membership_lines, _('Membership is not registrated.') membership_line = membership_lines[0] wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'account.invoice', membership_line.account_invoice_id.id, 'invoice_open', cr) - | - I'm checking "Current membership state" of "Mark Johnson". It is an "Invoiced Member" or not. + I'm checking "Current membership state" of "Seagate". It is an "Invoiced Member" or not. - - !assert {model: res.partner, id: res_partner_markjohnson0}: + !assert {model: res.partner, id: base.res_partner_seagate}: - membership_state == 'invoiced', 'Member should be has "Current Membership State" in "Invoiced Member".' - | @@ -85,10 +71,10 @@ - membership_state == 'free', 'Member should be has "Current Membership State" in "Free Member".' - | - I'm set "Mark Johnson" as a associated member of "Ms. Johnson" and also set Non free member. + I'm set "Seagate" as a associated member of "Ms. Johnson" and also set Non free member. - !python {model: res.partner}: | - self.write(cr, uid, [ref("res_partner_msjohnson0")], {'free_member': False, 'associate_member': ref("res_partner_markjohnson0")}) + self.write(cr, uid, [ref("res_partner_msjohnson0")], {'free_member': False, 'associate_member': ref("base.res_partner_seagate")}) - | I'm checking "Current membership state" of "Ms. Johnson". It is an "Paid Member" or not. @@ -108,17 +94,17 @@ type: service list_price: 50.00 - | - I'm making invoice of "Mark Johnson" member on joining new membership "Regular Membership". + I'm making invoice of "Seagate" member on joining new membership "Regular Membership". - !python {model: res.partner}: | - self.create_membership_invoice(cr, uid, [ref("res_partner_markjohnson0")], product_id=ref("product_product_membershipproduct1"), datas={"amount":50.00}) + self.create_membership_invoice(cr, uid, [ref("base.res_partner_seagate")], product_id=ref("product_product_membershipproduct1"), datas={"amount":50.00}) - | - I'm checking "Current membership state" of "Mark Johnson". It is an "Old Member" or not. + I'm checking "Current membership state" of "Seagate". It is an "Old Member" or not. - - !assert {model: res.partner, id: res_partner_markjohnson0}: + !assert {model: res.partner, id: base.res_partner_seagate}: - membership_state == 'old', 'Member should be has "Current Membership State" in "Old Member".' - | - I'm doing to make credit note of invoice which is paid by "Mark Johnson" to cancel membership. + I'm doing to make credit note of invoice which is paid by "Seagate" to cancel membership. - !python {model: account.invoice}: | from tools.translate import _ @@ -128,15 +114,15 @@ membership_pool = self.pool.get('product.product') invoice_refund_pool = self.pool.get('account.invoice.refund') - membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('res_partner_markjohnson0'))]) + membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('base.res_partner_seagate'))]) membership_lines = membership_line_pool.browse(cr, uid, membership_line_ids) assert membership_lines, _('Membership is not registrated.') membership_line = membership_lines[0] refund_id = invoice_refund_pool.create(cr, uid, {'description': 'Refund of Membership', 'filter_refund': 'refund'}, {'active_id': membership_line.account_invoice_id.id}) invoice_refund_pool.invoice_refund(cr, uid, [refund_id], {'active_id': membership_line.account_invoice_id.id, 'active_ids': [membership_line.account_invoice_id.id]}) - | - I'm checking "Current membership state" of "Mark Johnson". It is an "Cancelled Member" or not. + I'm checking "Current membership state" of "Seagate". It is an "Cancelled Member" or not. - - !assert {model: res.partner, id: res_partner_markjohnson0}: + !assert {model: res.partner, id: base.res_partner_seagate}: - membership_state == 'canceled', 'Member should be has "Current Membership State" in "Cancelled Member".' diff --git a/addons/purchase/__init__.py b/addons/purchase/__init__.py index 301e32ebca9..66f92a60831 100644 --- a/addons/purchase/__init__.py +++ b/addons/purchase/__init__.py @@ -26,6 +26,7 @@ import wizard import report import stock import company +import edi # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/purchase/__openerp__.py b/addons/purchase/__openerp__.py index 901fb6d3e98..b4f3a9adf30 100644 --- a/addons/purchase/__openerp__.py +++ b/addons/purchase/__openerp__.py @@ -40,7 +40,7 @@ Dashboard for purchase management that includes: 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', 'images' : ['images/purchase_order.jpeg', 'images/purchase_analysis.jpeg', 'images/request_for_quotation.jpeg'], - 'depends': ['base', 'account', 'stock', 'process', 'procurement'], + 'depends': ['stock', 'process', 'procurement'], 'data': [ 'security/purchase_security.xml', 'security/ir.model.access.csv', @@ -58,15 +58,17 @@ Dashboard for purchase management that includes: 'process/purchase_process.xml', 'report/purchase_report_view.xml', 'board_purchase_view.xml', + 'edi/purchase_order_action_data.xml', ], 'test': [ - 'test/process/cancel_order.yml', + 'test/process/cancel_order.yml', 'test/process/rfq2order2done.yml', 'test/process/generate_invoice_from_reception.yml', - 'test/process/run_scheduler.yml', - 'test/process/merge_order.yml', + 'test/process/run_scheduler.yml', + 'test/process/merge_order.yml', + 'test/process/edi_purchase_order.yml', 'test/ui/print_report.yml', - 'test/ui/duplicate_order.yml', + 'test/ui/duplicate_order.yml', 'test/ui/delete_order.yml', ], 'demo': [ diff --git a/addons/purchase/edi/__init__.py b/addons/purchase/edi/__init__.py new file mode 100644 index 00000000000..cfac0182d76 --- /dev/null +++ b/addons/purchase/edi/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import purchase_order \ No newline at end of file diff --git a/addons/purchase/edi/purchase_order.py b/addons/purchase/edi/purchase_order.py new file mode 100644 index 00000000000..235b02645ed --- /dev/null +++ b/addons/purchase/edi/purchase_order.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + +from osv import fields, osv, orm +from edi import EDIMixin +from tools import DEFAULT_SERVER_DATE_FORMAT +from tools.translate import _ + +PURCHASE_ORDER_LINE_EDI_STRUCT = { + 'name': True, + 'date_planned': True, + 'product_id': True, + 'product_uom': True, + 'price_unit': True, + 'product_qty': True, + 'notes': True, + + # fields used for web preview only - discarded on import + 'price_subtotal': True, +} + +PURCHASE_ORDER_EDI_STRUCT = { + 'company_id': True, # -> to be changed into partner + 'name': True, + 'partner_ref': True, + 'origin': True, + 'date_order': True, + 'partner_id': True, + #custom: 'partner_address', + 'notes': True, + 'order_line': PURCHASE_ORDER_LINE_EDI_STRUCT, + #custom: currency_id + + # fields used for web preview only - discarded on import + 'amount_total': True, + 'amount_untaxed': True, + 'amount_tax': True, +} + +class purchase_order(osv.osv, EDIMixin): + _inherit = 'purchase.order' + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + """Exports a purchase order""" + edi_struct = dict(edi_struct or PURCHASE_ORDER_EDI_STRUCT) + res_company = self.pool.get('res.company') + res_partner_address = self.pool.get('res.partner.address') + edi_doc_list = [] + for order in records: + # generate the main report + self._edi_generate_report_attachment(cr, uid, order, context=context) + + # Get EDI doc based on struct. The result will also contain all metadata fields and attachments. + edi_doc = super(purchase_order,self).edi_export(cr, uid, [order], edi_struct, context)[0] + edi_doc.update({ + # force trans-typing to purchase.order upon import + '__import_model': 'sale.order', + '__import_module': 'sale', + + 'company_address': res_company.edi_export_address(cr, uid, order.company_id, context=context), + 'partner_address': res_partner_address.edi_export(cr, uid, [order.partner_address_id], context=context)[0], + 'currency': self.pool.get('res.currency').edi_export(cr, uid, [order.pricelist_id.currency_id], + context=context)[0], + }) + if edi_doc.get('order_line'): + for line in edi_doc['order_line']: + line['__import_model'] = 'sale.order.line' + edi_doc_list.append(edi_doc) + return edi_doc_list + + def edi_import_company(self, cr, uid, edi_document, context=None): + # TODO: for multi-company setups, we currently import the document in the + # user's current company, but we should perhaps foresee a way to select + # the desired company among the user's allowed companies + + self._edi_requires_attributes(('company_id','company_address'), edi_document) + res_partner_address = self.pool.get('res.partner.address') + res_partner = self.pool.get('res.partner') + + # imported company = as a new partner + src_company_id, src_company_name = edi_document.pop('company_id') + partner_id = self.edi_import_relation(cr, uid, 'res.partner', src_company_name, + src_company_id, context=context) + partner_value = {'customer': True} + res_partner.write(cr, uid, [partner_id], partner_value, context=context) + + # imported company_address = new partner address + address_info = edi_document.pop('company_address') + address_info['partner_id'] = (src_company_id, src_company_name) + address_info['type'] = 'default' + address_id = res_partner_address.edi_import(cr, uid, address_info, context=context) + + # modify edi_document to refer to new partner/address + partner_address = res_partner_address.browse(cr, uid, address_id, context=context) + edi_document['partner_id'] = (src_company_id, src_company_name) + edi_document.pop('partner_address', False) # ignored + edi_document['partner_address_id'] = self.edi_m2o(cr, uid, partner_address, context=context) + + return partner_id + + def _edi_get_pricelist(self, cr, uid, partner_id, currency, context=None): + # TODO: refactor into common place for purchase/sale, e.g. into product module + partner_model = self.pool.get('res.partner') + partner = partner_model.browse(cr, uid, partner_id, context=context) + pricelist = partner.property_product_pricelist_purchase + if not pricelist: + pricelist = self.pool.get('ir.model.data').get_object(cr, uid, 'purchase', 'list0', context=context) + + if not pricelist.currency_id == currency: + # look for a pricelist with the right type and currency, or make a new one + pricelist_type = 'purchase' + product_pricelist = self.pool.get('product.pricelist') + match_pricelist_ids = product_pricelist.search(cr, uid,[('type','=',pricelist_type), + ('currency_id','=',currency.id)]) + if match_pricelist_ids: + pricelist_id = match_pricelist_ids[0] + else: + pricelist_name = _('EDI Pricelist (%s)') % (currency.name,) + pricelist_id = product_pricelist.create(cr, uid, {'name': pricelist_name, + 'type': pricelist_type, + 'currency_id': currency.id, + }) + self.pool.get('product.pricelist.version').create(cr, uid, {'name': pricelist_name, + 'pricelist_id': pricelist_id}) + pricelist = product_pricelist.browse(cr, uid, pricelist_id) + + return self.edi_m2o(cr, uid, pricelist, context=context) + + def _edi_get_location(self, cr, uid, partner_id, context=None): + partner_model = self.pool.get('res.partner') + partner = partner_model.browse(cr, uid, partner_id, context=context) + location = partner.property_stock_customer + if not location: + location = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context) + return self.edi_m2o(cr, uid, location, context=context) + + def edi_import(self, cr, uid, edi_document, context=None): + self._edi_requires_attributes(('company_id','company_address','order_line','date_order','currency'), edi_document) + + #import company as a new partner + partner_id = self.edi_import_company(cr, uid, edi_document, context=context) + + # currency for rounding the discount calculations and for the pricelist + res_currency = self.pool.get('res.currency') + currency_info = edi_document.pop('currency') + currency_id = res_currency.edi_import(cr, uid, currency_info, context=context) + order_currency = res_currency.browse(cr, uid, currency_id) + + partner_ref = edi_document.pop('partner_ref', False) + edi_document['partner_ref'] = edi_document['name'] + edi_document['name'] = partner_ref or edi_document['name'] + edi_document['pricelist_id'] = self._edi_get_pricelist(cr, uid, partner_id, order_currency, context=context) + edi_document['location_id'] = self._edi_get_location(cr, uid, partner_id, context=context) + + # discard web preview fields, if present + edi_document.pop('amount_total', None) + edi_document.pop('amount_tax', None) + edi_document.pop('amount_untaxed', None) + edi_document.pop('payment_term', None) + edi_document.pop('order_policy', None) + edi_document.pop('user_id', None) + + for order_line in edi_document['order_line']: + self._edi_requires_attributes(('date_planned', 'product_id', 'product_uom', 'product_qty', 'price_unit'), order_line) + # original sale order contains unit price and discount, but not final line price + discount = order_line.pop('discount', 0.0) + if discount: + order_line['price_unit'] = res_currency.round(cr, uid, order_currency, + (order_line['price_unit'] * (1 - (discount or 0.0) / 100.0))) + # sale order lines have sequence numbers, not purchase order lines + order_line.pop('sequence', None) + + # discard web preview fields, if present + order_line.pop('price_subtotal', None) + return super(purchase_order,self).edi_import(cr, uid, edi_document, context=context) + +class purchase_order_line(osv.osv, EDIMixin): + _inherit='purchase.order.line' + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/purchase/edi/purchase_order_action_data.xml b/addons/purchase/edi/purchase_order_action_data.xml new file mode 100644 index 00000000000..f3f610b8e75 --- /dev/null +++ b/addons/purchase/edi/purchase_order_action_data.xml @@ -0,0 +1,156 @@ + + + + + + if not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='purchase.email_template_edi_purchase', context=context) + code + ir.actions.server + + True + Auto-email confirmed purchase orders + + + + + Email Templates + email.template + form + form,tree + + + + + + + + + + + + + + + + + + Automated Purchase Order Notification Mail + ${object.validator.user_email or ''} + ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) + ${object.partner_address_id.email} + + + + +

Hello${object.partner_address_id.name and ' ' or ''}${object.partner_address_id.name or ''},

+ +

Here is a purchase order confirmation from ${object.company_id.name}:

+ +

+   REFERENCES
+   Order number: ${object.name}
+   Order total: ${object.amount_total} ${object.pricelist_id.currency_id.name}
+   Order date: ${object.date_order}
+ % if object.origin: +   Order reference: ${object.origin}
+ % endif + % if object.partner_ref: +   Your reference: ${object.partner_ref}
+ % endif +   Your contact: ${object.validator.name} +

+ +

+ You can view the order confirmation document and download it using the following link: +

+ View Order + +
+

If you have any question, do not hesitate to contact us.

+

Thank you!

+
+
+
+

+ ${object.company_id.name}

+
+
+ + % if object.company_id.street: + ${object.company_id.street}
+ % endif + % if object.company_id.street2: + ${object.company_id.street2}
+ % endif + % if object.company_id.city or object.company_id.zip: + ${object.company_id.zip} ${object.company_id.city}
+ % endif + % if object.company_id.country_id: + ${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}
+ % endif +
+ % if object.company_id.phone: +
+ Phone:  ${object.company_id.phone} +
+ % endif + % if object.company_id.website: + + %endif +

+
+
+ ]]>
+ + % endif + | Your contact: ${object.validator.name} ${object.validator.user_email and '<%s>'%(object.validator.user_email) or ''} + +You can view the order confirmation and download it using the following link: + ${ctx.get('edi_web_url_view') or 'n/a'} + +If you have any question, do not hesitate to contact us. + +Thank you! + + +-- +${object.validator.name} ${object.validator.user_email and '<%s>'%(object.validator.user_email) or ''} +${object.company_id.name} +% if object.company_id.street: +${object.company_id.street or ''} +% endif +% if object.company_id.street2: +${object.company_id.street2} +% endif +% if object.company_id.city or object.company_id.zip: +${object.company_id.zip or ''} ${object.company_id.city or ''} +% endif +% if object.company_id.country_id: +${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''} +% endif +% if object.company_id.phone: +Phone: ${object.company_id.phone} +% endif +% if object.company_id.website: +${object.company_id.website or ''} +% endif + ]]> + + + \ No newline at end of file diff --git a/addons/purchase/purchase_report.xml b/addons/purchase/purchase_report.xml index e13ca9911b0..a3a4a6a70b6 100644 --- a/addons/purchase/purchase_report.xml +++ b/addons/purchase/purchase_report.xml @@ -1,7 +1,11 @@ - - + + diff --git a/addons/purchase/test/process/edi_purchase_order.yml b/addons/purchase/test/process/edi_purchase_order.yml new file mode 100644 index 00000000000..ff49b41157b --- /dev/null +++ b/addons/purchase/test/process/edi_purchase_order.yml @@ -0,0 +1,136 @@ +- + I create a draft Purchase Order +- + !record {model: purchase.order, id: purchase_order_edi_1}: + partner_id: base.res_partner_agrolait + partner_address_id: base.res_partner_address_8invoice + location_id: stock.stock_location_3 + pricelist_id: 1 + order_line: + - product_id: product.product_product_pc1 + product_qty: 1.0 + product_uom: 1 + price_unit: 150.0 + name: 'Basic PC' + date_planned: '2011-08-31' + order_line: + - product_id: product.product_product_pc3 + product_qty: 10.0 + product_uom: 1 + price_unit: 20.0 + name: 'Medium PC' + date_planned: '2011-08-31' + +- + I confirm the purchase order +- + !workflow {model: purchase.order, ref: purchase_order_edi_1, action: purchase_confirm} +- + Then I export the purchase order via EDI +- + !python {model: edi.document}: | + order_pool = self.pool.get('purchase.order') + order = order_pool.browse(cr, uid, ref("purchase_order_edi_1")) + token = self.export_edi(cr, uid, [order]) + assert token, 'Invalid EDI Token' + +- + Then I import a sample EDI document of a sale order +- + !python {model: edi.document}: | + purchase_order_pool = self.pool.get('purchase.order') + edi_document = { + "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_test", + "__module": "sale", + "__model": "sale.order", + "__import_module": "purchase", + "__import_model": "purchase.order", + "__version": [6,1,0], + "name": "SO008", + "currency": { + "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.EUR", + "__module": "base", + "__model": "res.currency", + "code": "EUR", + "symbol": "€", + }, + "date_order": "2011-09-13", + "partner_id": ["sale:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_test22", "Junjun wala"], + "partner_address": { + "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_address_7wdsjasdjh", + "__module": "base", + "__model": "res.partner.address", + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"], + }, + "company_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_company", "Supplier S.A."], + "company_address": { + "__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_address", + "__module": "base", + "__model": "res.partner.address", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.be", "Belgium"], + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "street2": "mailbox 34", + "bank_ids": [ + ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_bank-XiwqnxKWzGbp","Guys bank: 123477777-156113"] + ], + }, + "order_line": [{ + "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeuI-SSP0", + "__module": "sale", + "__model": "sale.order.line", + "__import_module": "purchase", + "__import_model": "purchase.order.line", + "name": "Basic PC", + "product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"], + "product_qty": 1.0, + "date_planned": "2011-09-30", + "sequence": 10, + "price_unit": 150.0, + "product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc1", "[PC1] Basic PC"], + }, + { + "__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeadasdad", + "__module": "sale", + "__model": "sale.order.line", + "__import_module": "purchase", + "__import_model": "purchase.order.line", + "name": "Medium PC", + "product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"], + "product_qty": 10.0, + "sequence": 11, + "date_planned": "2011-09-15", + "price_unit": 20.0, + "product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc3", "[PC3] Medium PC"], + }], + } + new_purchase_order_id = purchase_order_pool.edi_import(cr, uid, edi_document, context=context) + assert new_purchase_order_id, 'Purchase order import failed' + order_new = purchase_order_pool.browse(cr, uid, new_purchase_order_id) + + # check bank info on partner + assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner" + bank_info = order_new.partner_id.bank_ids[0] + assert bank_info.acc_number == "Guys bank: 123477777-156113", 'Expected "Guys bank: 123477777-156113", got %s' % bank_info.acc_number + + assert order_new.pricelist_id.name == 'Default Purchase Pricelist' , "Default Purchase Pricelist was not automatically assigned" + assert order_new.amount_total == 350, "Amount total is not same" + assert order_new.amount_untaxed == 350, "untaxed amount is not same" + assert len(order_new.order_line) == 2, "Purchase order lines number mismatch" + for purchase_line in order_new.order_line: + if purchase_line.name == 'Basic PC': + assert purchase_line.product_uom.name == "PCE" , "uom is not same" + assert purchase_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(purchase_line.price_unit,) + assert purchase_line.product_qty == 1 , "product qty is not same" + elif purchase_line.name == 'Medium PC': + assert purchase_line.product_uom.name == "PCE" , "uom is not same" + assert purchase_line.price_unit == 20 , "unit price is not same, got %s, expected 20"%(purchase_line.price_unit,) + assert purchase_line.product_qty == 10 , "product qty is not same" + else: + raise AssertionError('unknown order line: %s' % purchase_line) diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py index c7be8e7e14b..b26486e1425 100644 --- a/addons/sale/__init__.py +++ b/addons/sale/__init__.py @@ -28,5 +28,6 @@ import stock import wizard import report import company +import edi # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index 76f99024fba..8b5ae108f72 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -83,9 +83,11 @@ Dashboard for Sales Manager that includes: 'stock_view.xml', 'process/sale_process.xml', 'board_sale_view.xml', + 'edi/sale_order_action_data.xml', ], 'demo_xml': ['sale_demo.xml'], 'test': [ + 'test/edi_sale_order.yml', 'test/data_test.yml', 'test/manual_order_policy.yml', 'test/prepaid_order_policy.yml', diff --git a/addons/sale/edi/__init__.py b/addons/sale/edi/__init__.py new file mode 100644 index 00000000000..566b1714ffd --- /dev/null +++ b/addons/sale/edi/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import sale_order \ No newline at end of file diff --git a/addons/sale/edi/sale_order.py b/addons/sale/edi/sale_order.py new file mode 100644 index 00000000000..d40d0b613ed --- /dev/null +++ b/addons/sale/edi/sale_order.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2011 OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + +from osv import fields, osv, orm +from edi import EDIMixin +from tools import DEFAULT_SERVER_DATE_FORMAT + +SALE_ORDER_LINE_EDI_STRUCT = { + 'sequence': True, + 'name': True, + #custom: 'date_planned' + 'product_id': True, + 'product_uom': True, + 'price_unit': True, + #custom: 'product_qty' + 'discount': True, + 'notes': True, + + # fields used for web preview only - discarded on import + 'price_subtotal': True, +} + +SALE_ORDER_EDI_STRUCT = { + 'name': True, + 'origin': True, + 'company_id': True, # -> to be changed into partner + #custom: 'partner_ref' + 'date_order': True, + 'partner_id': True, + #custom: 'partner_address' + #custom: 'notes' + 'order_line': SALE_ORDER_LINE_EDI_STRUCT, + + # fields used for web preview only - discarded on import + 'amount_total': True, + 'amount_untaxed': True, + 'amount_tax': True, + 'payment_term': True, + 'order_policy': True, + 'user_id': True, +} + +class sale_order(osv.osv, EDIMixin): + _inherit = 'sale.order' + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + """Exports a Sale order""" + edi_struct = dict(edi_struct or SALE_ORDER_EDI_STRUCT) + res_company = self.pool.get('res.company') + res_partner_address = self.pool.get('res.partner.address') + edi_doc_list = [] + for order in records: + # generate the main report + self._edi_generate_report_attachment(cr, uid, order, context=context) + + # Get EDI doc based on struct. The result will also contain all metadata fields and attachments. + edi_doc = super(sale_order,self).edi_export(cr, uid, [order], edi_struct, context)[0] + edi_doc.update({ + # force trans-typing to purchase.order upon import + '__import_model': 'purchase.order', + '__import_module': 'purchase', + + 'company_address': res_company.edi_export_address(cr, uid, order.company_id, context=context), + 'partner_address': res_partner_address.edi_export(cr, uid, [order.partner_order_id], context=context)[0], + + 'currency': self.pool.get('res.currency').edi_export(cr, uid, [order.pricelist_id.currency_id], + context=context)[0], + 'partner_ref': order.client_order_ref or False, + 'notes': order.note or False, + }) + edi_doc_list.append(edi_doc) + return edi_doc_list + + + def _edi_import_company(self, cr, uid, edi_document, context=None): + # TODO: for multi-company setups, we currently import the document in the + # user's current company, but we should perhaps foresee a way to select + # the desired company among the user's allowed companies + + self._edi_requires_attributes(('company_id','company_address'), edi_document) + res_partner_address = self.pool.get('res.partner.address') + res_partner = self.pool.get('res.partner') + + # imported company = as a new partner + src_company_id, src_company_name = edi_document.pop('company_id') + partner_id = self.edi_import_relation(cr, uid, 'res.partner', src_company_name, + src_company_id, context=context) + partner_value = {'supplier': True} + res_partner.write(cr, uid, [partner_id], partner_value, context=context) + + # imported company_address = new partner address + address_info = edi_document.pop('company_address') + address_info['partner_id'] = (src_company_id, src_company_name) + address_info['type'] = 'default' + address_id = res_partner_address.edi_import(cr, uid, address_info, context=context) + + # modify edi_document to refer to new partner/address + partner_address = res_partner_address.browse(cr, uid, address_id, context=context) + edi_document['partner_id'] = (src_company_id, src_company_name) + edi_document.pop('partner_address', False) # ignored + address_edi_m2o = self.edi_m2o(cr, uid, partner_address, context=context) + edi_document['partner_order_id'] = address_edi_m2o + edi_document['partner_invoice_id'] = address_edi_m2o + edi_document['partner_shipping_id'] = address_edi_m2o + + return partner_id + + def _edi_get_pricelist(self, cr, uid, partner_id, currency, context=None): + # TODO: refactor into common place for purchase/sale, e.g. into product module + partner_model = self.pool.get('res.partner') + partner = partner_model.browse(cr, uid, partner_id, context=context) + pricelist = partner.property_product_pricelist + if not pricelist: + pricelist = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'list0', context=context) + + if not pricelist.currency_id == currency: + # look for a pricelist with the right type and currency, or make a new one + pricelist_type = 'sale' + product_pricelist = self.pool.get('product.pricelist') + match_pricelist_ids = product_pricelist.search(cr, uid,[('type','=',pricelist_type), + ('currency_id','=',currency.id)]) + if match_pricelist_ids: + pricelist_id = match_pricelist_ids[0] + else: + pricelist_name = _('EDI Pricelist (%s)') % (currency.name,) + pricelist_id = product_pricelist.create(cr, uid, {'name': pricelist_name, + 'type': pricelist_type, + 'currency_id': currency.id, + }) + self.pool.get('product.pricelist.version').create(cr, uid, {'name': pricelist_name, + 'pricelist_id': pricelist_id}) + pricelist = product_pricelist.browse(cr, uid, pricelist_id) + + return self.edi_m2o(cr, uid, pricelist, context=context) + + def edi_import(self, cr, uid, edi_document, context=None): + self._edi_requires_attributes(('company_id','company_address','order_line','date_order','currency'), edi_document) + + #import company as a new partner + partner_id = self._edi_import_company(cr, uid, edi_document, context=context) + + # currency for rounding the discount calculations and for the pricelist + res_currency = self.pool.get('res.currency') + currency_info = edi_document.pop('currency') + currency_id = res_currency.edi_import(cr, uid, currency_info, context=context) + order_currency = res_currency.browse(cr, uid, currency_id) + + date_order = edi_document['date_order'] + partner_ref = edi_document.pop('partner_ref', False) + edi_document['client_order_ref'] = edi_document['name'] + edi_document['name'] = partner_ref or edi_document['name'] + edi_document['note'] = edi_document.pop('notes', False) + edi_document['pricelist_id'] = self._edi_get_pricelist(cr, uid, partner_id, order_currency, context=context) + + # discard web preview fields, if present + edi_document.pop('amount_total', None) + edi_document.pop('amount_tax', None) + edi_document.pop('amount_untaxed', None) + + order_lines = edi_document['order_line'] + for order_line in order_lines: + self._edi_requires_attributes(('date_planned', 'product_id', 'product_uom', 'product_qty', 'price_unit'), order_line) + order_line['product_uom_qty'] = order_line['product_qty'] + del order_line['product_qty'] + date_planned = order_line.pop('date_planned') + delay = 0 + if date_order and date_planned: + # no security_days buffer, this is the promised date given by supplier + delay = (datetime.strptime(date_planned, DEFAULT_SERVER_DATE_FORMAT) - \ + datetime.strptime(date_order, DEFAULT_SERVER_DATE_FORMAT)).days + order_line['delay'] = delay + + # discard web preview fields, if present + order_line.pop('price_subtotal', None) + return super(sale_order,self).edi_import(cr, uid, edi_document, context=context) + +class sale_order_line(osv.osv, EDIMixin): + _inherit='sale.order.line' + + def edi_export(self, cr, uid, records, edi_struct=None, context=None): + """Overridden to provide sale order line fields with the expected names + (sale and purchase orders have different column names)""" + edi_struct = dict(edi_struct or SALE_ORDER_LINE_EDI_STRUCT) + edi_doc_list = [] + for line in records: + edi_doc = super(sale_order_line,self).edi_export(cr, uid, [line], edi_struct, context)[0] + edi_doc['__import_model'] = 'purchase.order.line' + edi_doc['product_qty'] = line.product_uom_qty + if line.product_uos: + edi_doc.update(product_uom=line.product_uos, + product_qty=line.product_uos_qty) + + # company.security_days is for internal use, so customer should only + # see the expected date_planned based on line.delay + date_planned = datetime.strptime(line.order_id.date_order, DEFAULT_SERVER_DATE_FORMAT) + \ + relativedelta(days=line.delay or 0.0) + edi_doc['date_planned'] = date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT) + edi_doc_list.append(edi_doc) + return edi_doc_list + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/sale/edi/sale_order_action_data.xml b/addons/sale/edi/sale_order_action_data.xml new file mode 100644 index 00000000000..3d7a554ff4a --- /dev/null +++ b/addons/sale/edi/sale_order_action_data.xml @@ -0,0 +1,193 @@ + + + + + + if not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='sale.email_template_edi_sale', context=context) + code + ir.actions.server + + True + Auto-email confirmed sale orders + + + + + Email Templates + email.template + form + form,tree + + + + + + + + + + + + + + + + + + + + Automated Sale Order Notification Mail + ${object.user_id.user_email or ''} + ${object.company_id.name} Order (Ref ${object.name or 'n/a' }) + ${object.partner_invoice_id.email} + + + + +

Hello${object.partner_order_id.name and ' ' or ''}${object.partner_order_id.name or ''},

+ +

Here is your order confirmation for ${object.partner_id.name}:

+ +

+   REFERENCES
+   Order number: ${object.name}
+   Order total: ${object.amount_total} ${object.pricelist_id.currency_id.name}
+   Order date: ${object.date_order}
+ % if object.origin: +   Order reference: ${object.origin}
+ % endif + % if object.client_order_ref: +   Your reference: ${object.client_order_ref}
+ % endif +   Your contact: ${object.user_id.name} +

+ +

+ You can view the order confirmation document, download it and pay online using the following link: +

+ View Order + + % if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account: + <% + comp_name = quote(object.company_id.name) + order_name = quote(object.name) + paypal_account = quote(object.company_id.paypal_account) + order_amount = quote(str(object.amount_total)) + cur_name = quote(object.pricelist_id.currency_id.name) + paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Order%%20%s" \ + "&invoice=%s&amount=%s&currency_code=%s&button_subtype=services&no_note=1" \ + "&bn=OpenERP_Order_PayNow_%s" % \ + (paypal_account,comp_name,order_name,order_name,order_amount,cur_name,cur_name) + %> +
+

It is also possible to directly pay with Paypal:

+ + + + % endif + +
+

If you have any question, do not hesitate to contact us.

+

Thank you for choosing ${object.company_id.name or 'us'}!

+
+
+
+

+ ${object.company_id.name}

+
+
+ + % if object.company_id.street: + ${object.company_id.street}
+ % endif + % if object.company_id.street2: + ${object.company_id.street2}
+ % endif + % if object.company_id.city or object.company_id.zip: + ${object.company_id.zip} ${object.company_id.city}
+ % endif + % if object.company_id.country_id: + ${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}
+ % endif +
+ % if object.company_id.phone: +
+ Phone:  ${object.company_id.phone} +
+ % endif + % if object.company_id.website: + + %endif +

+
+ + ]]>
+ + % endif + | Your contact: ${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''} + +You can view the order confirmation, download it and even pay online using the following link: + ${ctx.get('edi_web_url_view') or 'n/a'} + +% if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account: +<% +comp_name = quote(object.company_id.name) +order_name = quote(object.name) +paypal_account = quote(object.company_id.paypal_account) +order_amount = quote(str(object.amount_total)) +cur_name = quote(object.pricelist_id.currency_id.name) +paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Order%%20%s&invoice=%s&amount=%s" \ + "¤cy_code=%s&button_subtype=services&no_note=1&bn=OpenERP_Order_PayNow_%s" % \ + (paypal_account,comp_name,order_name,order_name,order_amount,cur_name,cur_name) +%> +It is also possible to directly pay with Paypal: + ${paypal_url} +% endif + +If you have any question, do not hesitate to contact us. + + +Thank you for choosing ${object.company_id.name}! + + +-- +${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''} +${object.company_id.name} +% if object.company_id.street: +${object.company_id.street or ''} +% endif +% if object.company_id.street2: +${object.company_id.street2} +% endif +% if object.company_id.city or object.company_id.zip: +${object.company_id.zip or ''} ${object.company_id.city or ''} +% endif +% if object.company_id.country_id: +${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''} +% endif +% if object.company_id.phone: +Phone: ${object.company_id.phone} +% endif +% if object.company_id.website: +${object.company_id.website or ''} +% endif + ]]> +
+
+
diff --git a/addons/sale/sale.py b/addons/sale/sale.py index d68789f4459..f9c356b7d0c 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -217,7 +217,7 @@ class sale_order(osv.osv): 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."), 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."), - 'picking_policy': fields.selection([('direct', 'Deliver each products when available'), ('one', 'Deliver all products at once')], + 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')], 'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""), 'order_policy': fields.selection([ ('prepaid', 'Pay before delivery'), diff --git a/addons/sale/sale_report.xml b/addons/sale/sale_report.xml index 33819df0bde..0f77913a3c9 100644 --- a/addons/sale/sale_report.xml +++ b/addons/sale/sale_report.xml @@ -2,7 +2,9 @@ - + diff --git a/addons/sale/test/data_test.yml b/addons/sale/test/data_test.yml index 0be8474754c..8b5d32a086a 100644 --- a/addons/sale/test/data_test.yml +++ b/addons/sale/test/data_test.yml @@ -174,6 +174,7 @@ category_id: - res_partner_category_customers0 name: Cleartrail + opt_out: True - I create contact address for Cleartrail. - diff --git a/addons/sale/test/edi_sale_order.yml b/addons/sale/test/edi_sale_order.yml new file mode 100644 index 00000000000..9a207d9f44e --- /dev/null +++ b/addons/sale/test/edi_sale_order.yml @@ -0,0 +1,132 @@ +- + I create a draft Sale Order +- + !record {model: sale.order, id: sale_order_edi_1}: + partner_id: base.res_partner_agrolait + partner_invoice_id: base.res_partner_address_8invoice + partner_order_id: base.res_partner_address_8invoice + partner_shipping_id: base.res_partner_address_8invoice + pricelist_id: 1 + order_line: + - product_id: product.product_product_pc1 + product_uom_qty: 1.0 + product_uom: 1 + price_unit: 150.0 + name: 'Basic pc' + order_line: + - product_id: product.product_product_pc3 + product_uom_qty: 10.0 + product_uom: 1 + price_unit: 200.0 + name: 'Medium pc' +- + I confirm the sale order +- + !workflow {model: sale.order, ref: sale_order_edi_1, action: order_confirm} +- + Then I export the sale order via EDI +- + !python {model: edi.document}: | + sale_order = self.pool.get('sale.order') + so = sale_order.browse(cr, uid, ref("sale_order_edi_1")) + token = self.export_edi(cr, uid, [so]) + assert token, 'Invalid EDI Token' +- + Then I import a sample EDI document of a purchase order +- + !python {model: edi.document}: | + sale_order_pool = self.pool.get('sale.order') + edi_document = { + "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test", + "__module": "purchase", + "__model": "purchase.order", + "__import_module": "sale", + "__import_model": "sale.order", + "__version": [6,1,0], + "name": "PO00011", + "date_order": "2011-09-12", + "currency": { + "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.EUR", + "__module": "base", + "__model": "res.currency", + "code": "EUR", + "symbol": "€", + }, + "company_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.main_company", "Client S.A."], + "company_address": { + "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.some_address", + "__module": "base", + "__model": "res.partner.address", + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"], + "bank_ids": [ + ["base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"] + ], + }, + "partner_id": ["purchase:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_test20", "jones white"], + "partner_address": { + "__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh", + "__module": "base", + "__model": "res.partner.address", + "phone": "(+32).81.81.37.00", + "street": "Chaussee de Namur 40", + "city": "Gerompont", + "zip": "1367", + "country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"], + }, + "order_line": [{ + "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-AlhsVDZGoKvJ", + "__module": "purchase", + "__model": "purchase.order.line", + "__import_module": "sale", + "__import_model": "sale.order.line", + "name": "Basic PC", + "date_planned": "2011-09-30", + "price_unit": 150.0, + "product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_pc1", "[PC1] Basic PC"], + "product_qty": 1.0, + "product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "PCE"], + }, + { + "__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-Alsads33e", + "__module": "purchase", + "__model": "purchase.order.line", + "__import_module": "sale", + "__import_model": "sale.order.line", + "name": "Medium PC", + "date_planned": "2011-09-15", + "price_unit": 100.0, + "product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_pc3", "[PC3] Medium PC"], + "product_qty": 2.0, + "product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "PCE"], + }], + } + new_sale_order_id = sale_order_pool.edi_import(cr, uid, edi_document, context=context) + assert new_sale_order_id, 'Sale order import failed' + order_new = sale_order_pool.browse(cr, uid, new_sale_order_id) + + # check bank info on partner + assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner" + bank_info = order_new.partner_id.bank_ids[0] + assert bank_info.acc_number == "Ladies bank: 032465789-156113", 'Expected "Ladies bank: 032465789-156113", got %s' % bank_info.acc_number + + assert order_new.pricelist_id.name == 'Public Pricelist' , "Public Price list was not automatically assigned" + assert order_new.amount_total == 350, "Amount total is wrong" + assert order_new.amount_untaxed == 350, "Untaxed amount is wrong" + assert len(order_new.order_line) == 2, "Sale order lines mismatch" + for sale_line in order_new.order_line: + if sale_line.name == 'Basic PC': + assert sale_line.delay == 18 , "incorrect delay: got %s, expected 18"%(sale_line.delay,) + assert sale_line.product_uom.name == "PCE" , "uom is not same" + assert sale_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(sale_line.price_unit,) + assert sale_line.product_uom_qty == 1 , "product qty is not same" + elif sale_line.name == 'Medium PC': + assert sale_line.delay == 3 , "incorrect delay: got %s, expected 3"%(sale_line.delay,) + assert sale_line.product_uom.name == "PCE" , "uom is not same" + assert sale_line.price_unit == 100 , "unit price is not same, got %s, expected 100"%(sale_line.price_unit,) + assert sale_line.product_uom_qty == 2 , "product qty is not same" + else: + raise AssertionError('unknown order line: %s' % sale_line) \ No newline at end of file diff --git a/addons/stock/__init__.py b/addons/stock/__init__.py index 78abee6d31d..0a241209593 100644 --- a/addons/stock/__init__.py +++ b/addons/stock/__init__.py @@ -25,4 +25,4 @@ import product import report import wizard -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 698ac7c8546..73215505177 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -79,7 +79,7 @@ Thanks to the double entry management, the inventory controlling is powerful and "partner_view.xml", "report/report_stock_move_view.xml", "report/report_stock_view.xml", - "board_warehouse_view.xml" + "board_warehouse_view.xml", ], 'test': [ 'test/stock_test.yml',