From c04065abd8f62c9a211c8fa824f5eecf68e61b73 Mon Sep 17 00:00:00 2001 From: qdp-odoo Date: Tue, 5 May 2015 17:28:04 +0200 Subject: [PATCH] [IMP] accounting v9. Yeeeeaah --- addons/account/__init__.py | 15 +- addons/account/__openerp__.py | 152 +- addons/account/account.py | 3852 ++--------------- addons/account/account_analytic_line.py | 171 +- addons/account/account_bank.py | 100 +- addons/account/account_bank_statement.py | 1590 ++++--- addons/account/account_cash_statement.py | 362 -- addons/account/account_end_fy.xml | 20 - addons/account/account_financial_report.py | 139 - .../account/account_financial_report_data.xml | 58 - addons/account/account_installer.xml | 56 - addons/account/account_invoice.py | 1594 +++---- addons/account/account_journal_dashboard.py | 313 ++ addons/account/account_menuitem.xml | 69 - addons/account/account_move.py | 1188 +++++ addons/account/account_move_line.py | 1429 ------ addons/account/account_payment.py | 524 +++ addons/account/account_pre_install.yml | 22 - addons/account/account_report.xml | 159 - addons/account/account_view.xml | 2645 ----------- addons/account/chart_template.py | 892 ++++ addons/account/company.py | 124 +- addons/account/data/account_data.xml | 142 +- .../data/configurable_account_chart.xml | 557 --- addons/account/data/data_account_type.xml | 103 +- .../{edi => data}/invoice_action_data.xml | 15 - .../account/demo/account_bank_statement.xml | 84 - .../account/demo/account_bank_statement.yml | 75 + addons/account/demo/account_demo.xml | 153 +- addons/account/demo/account_invoice_demo.xml | 188 - addons/account/demo/account_invoice_demo.yml | 195 + addons/account/demo/account_minimal.xml | 441 -- .../demo/account_statement_operations.yml | 37 + .../doc}/account_anglo_saxon.ods | Bin .../doc}/account_anglo_saxon.pdf | Bin addons/account/edi/__init__.py | 22 - addons/account/edi/invoice.py | 289 -- addons/account/i18n/account.pot | 5 - addons/account/installer.py | 174 - addons/account/partner.py | 422 +- addons/account/product.py | 110 +- addons/account/report/__init__.py | 16 +- .../report/account_aged_partner_balance.py | 407 -- .../report/account_analytic_entries_report.py | 75 - .../account_analytic_entries_report_view.xml | 66 - addons/account/report/account_balance.py | 136 - .../account/report/account_central_journal.py | 114 - .../account/report/account_entries_report.py | 145 - .../report/account_entries_report_view.xml | 122 - .../report/account_financial_report.py | 192 +- .../account/report/account_general_journal.py | 166 - .../account/report/account_general_ledger.py | 314 -- .../account/report/account_invoice_report.py | 197 +- .../report/account_invoice_report_view.xml | 20 +- addons/account/report/account_journal.py | 215 - .../account/report/account_partner_balance.py | 263 -- .../account/report/account_partner_ledger.py | 320 -- .../account/report/account_print_overdue.py | 90 - addons/account/report/account_report.py | 288 -- addons/account/report/account_report_view.xml | 219 - .../account/report/account_treasury_report.py | 80 - .../report/account_treasury_report_view.xml | 72 - addons/account/report/common_report_header.py | 141 - addons/account/report/report_vat.py | 260 -- addons/account/res_config.py | 609 ++- addons/account/res_currency.py | 44 - addons/account/security/account_security.xml | 83 +- addons/account/security/ir.model.access.csv | 53 +- .../static/description/account_illu_02.png | Bin 20193 -> 0 bytes .../static/description/account_illu_03.png | Bin 34110 -> 0 bytes .../static/description/account_sc_00.png | Bin 122701 -> 80153 bytes .../static/description/account_sc_01.png | Bin 34544 -> 0 bytes .../static/description/account_sc_05.png | Bin 37443 -> 0 bytes .../static/description/account_sc_06.png | Bin 146495 -> 122701 bytes .../static/description/bazile.png | Bin .../static/description/icon.png | Bin addons/account/static/description/index.html | 124 +- addons/account/static/description/sc00.png | Bin 122701 -> 0 bytes addons/account/static/src/css/account.css | 26 + .../account_bank_statement_reconciliation.css | 334 -- .../src/css/account_journal_dashboard.css | 38 + .../src/css/account_move_line_quickadd.css | 8 - .../src/css/account_move_reconciliation.css | 19 - .../static/src/css/account_reconciliation.css | 379 ++ ...ation.scss => account_reconciliation.scss} | 120 +- addons/account/static/src/img/graph.png | Bin 0 -> 1722 bytes .../js/account_journal_dashboard_widget.js | 98 + .../static/src/js/account_payment_widget.js | 45 + .../src/js/account_reconciliation_widgets.js | 2629 +++++++++++ .../js/tour_bank_statement_reconciliation.js | 3 +- addons/account/static/src/js/widgets.js | 1896 -------- .../account_bank_statement_reconciliation.xml | 220 - .../src/xml/account_move_line_quickadd.xml | 22 - .../src/xml/account_move_reconciliation.xml | 37 - .../static/src/xml/account_payment.xml | 45 + .../static/src/xml/account_reconciliation.xml | 365 ++ .../account/test/account_bank_statement.yml | 59 +- .../account/test/account_cash_statement.yml | 60 +- .../account/test/account_change_currency.yml | 79 - .../account/test/account_customer_invoice.yml | 120 - .../account/test/account_fiscalyear_close.yml | 81 - addons/account/test/account_invoice_state.yml | 27 +- addons/account/test/account_minimal_test.xml | 302 ++ addons/account/test/account_period_close.yml | 45 - addons/account/test/account_report.yml | 10 +- addons/account/test/account_sequence_test.yml | 43 - .../account/test/account_supplier_invoice.yml | 98 - addons/account/test/account_test_users.yml | 32 - addons/account/test/account_use_model.yml | 73 - .../test/account_validate_account_move.yml | 118 - addons/account/test/chart_of_account.yml | 73 - addons/account/test/price_accuracy00.yml | 93 - addons/account/test/test_edi_invoice.yml | 266 -- addons/account/test/test_parent_structure.py | 49 - addons/account/test/test_parent_structure.yml | 20 - addons/account/tests/__init__.py | 18 +- addons/account/tests/account_test_classes.py | 36 + addons/account/tests/account_test_users.py | 37 + .../tests/test_account_customer_invoice.py | 106 + .../tests/test_account_move_closed_period.py | 37 +- .../tests/test_account_supplier_invoice.py | 45 + .../test_account_validate_account_move.py | 41 + .../test_bank_statement_reconciliation.py | 91 + ...test_bank_stmt_reconciliation_widget_ui.py | 10 +- addons/account/tests/test_chart_of_account.py | 14 + addons/account/tests/test_fiscal_position.py | 9 +- .../tests/test_manual_reconciliation.py | 15 + addons/account/tests/test_payment.py | 189 + addons/account/tests/test_reconciliation.py | 218 +- addons/account/tests/test_search.py | 63 +- addons/account/tests/test_tax.py | 161 +- addons/account/views/account.xml | 10 +- .../{ => views}/account_analytic_view.xml | 24 +- .../account/{ => views}/account_bank_view.xml | 4 +- .../account_end_fy.xml} | 2 +- .../{ => views}/account_invoice_view.xml | 387 +- .../{ => views}/account_invoice_workflow.xml | 1 - .../views/account_journal_dashboard_view.xml | 316 ++ addons/account/views/account_menuitem.xml | 28 + addons/account/views/account_payment_view.xml | 233 + addons/account/views/account_report.xml | 17 + .../account/{ => views}/account_tip_data.xml | 0 .../account/{ => views}/account_unit_test.xml | 22 +- addons/account/views/account_view.xml | 1904 ++++++++ addons/account/{ => views}/company_view.xml | 9 +- addons/account/{ => views}/partner_view.xml | 102 +- addons/account/{ => views}/product_view.xml | 8 +- .../views/report_agedpartnerbalance.xml | 133 - .../account/views/report_centraljournal.xml | 93 - addons/account/views/report_financial.xml | 128 - .../account/views/report_generaljournal.xml | 98 - addons/account/views/report_generalledger.xml | 133 - addons/account/views/report_invoice.xml | 25 +- addons/account/views/report_journal.xml | 88 - addons/account/views/report_overdue.xml | 118 - .../account/views/report_partnerbalance.xml | 125 - addons/account/views/report_partnerledger.xml | 131 - .../views/report_partnerledgerother.xml | 131 - .../views/report_salepurchasejournal.xml | 107 - addons/account/views/report_trialbalance.xml | 94 - addons/account/views/report_vat.xml | 68 - .../account/{ => views}/res_config_view.xml | 110 +- addons/account/wizard/__init__.py | 41 +- .../wizard/account_automatic_reconcile.py | 254 -- .../account_automatic_reconcile_view.xml | 67 - .../account/wizard/account_change_currency.py | 75 - .../wizard/account_change_currency_view.xml | 32 - addons/account/wizard/account_chart.py | 107 - addons/account/wizard/account_chart_view.xml | 44 - .../wizard/account_financial_report.py | 93 - .../wizard/account_financial_report_view.xml | 76 - .../wizard/account_fiscalyear_close.py | 276 -- .../wizard/account_fiscalyear_close_state.py | 68 - .../wizard/account_fiscalyear_close_state.xml | 36 - .../wizard/account_fiscalyear_close_view.xml | 43 - .../account/wizard/account_invoice_refund.py | 247 +- .../wizard/account_invoice_refund_view.xml | 6 +- .../account/wizard/account_invoice_state.py | 49 +- .../account/wizard/account_journal_select.py | 46 - .../wizard/account_journal_select_view.xml | 36 - .../wizard/account_log_followup_note.py | 19 + .../wizard/account_log_followup_note_view.xml | 33 + .../wizard/account_move_bank_reconcile.py | 60 - .../account_move_bank_reconcile_view.xml | 31 - .../account_move_line_reconcile_select.py | 51 - ...ccount_move_line_reconcile_select_view.xml | 31 - .../wizard/account_move_line_select.py | 68 - .../wizard/account_move_line_select_view.xml | 30 - .../account_move_line_unreconcile_select.py | 40 - ...ount_move_line_unreconcile_select_view.xml | 31 - .../account/wizard/account_move_reversal.py | 18 + .../wizard/account_move_reversal_view.xml | 41 + .../wizard/account_open_closed_fiscalyear.py | 49 - .../account_open_closed_fiscalyear_view.xml | 40 - addons/account/wizard/account_period_close.py | 59 - .../wizard/account_period_close_view.xml | 41 - addons/account/wizard/account_reconcile.py | 181 +- .../account_reconcile_partner_process.py | 99 - ...account_reconcile_partner_process_view.xml | 42 - .../account/wizard/account_reconcile_view.xml | 17 +- .../wizard/account_report_account_balance.py | 40 - .../account_report_account_balance_view.xml | 50 - .../account_report_aged_partner_balance.py | 85 - ...count_report_aged_partner_balance_view.xml | 52 - .../wizard/account_report_central_journal.py | 36 - .../account_report_central_journal_view.xml | 35 - .../account/wizard/account_report_common.py | 185 - .../wizard/account_report_common_account.py | 42 - .../wizard/account_report_common_journal.py | 51 - .../wizard/account_report_common_partner.py | 43 - .../wizard/account_report_common_view.xml | 54 - .../wizard/account_report_general_journal.py | 36 - .../account_report_general_journal_view.xml | 37 - .../wizard/account_report_general_ledger.py | 64 - .../account_report_general_ledger_view.xml | 65 - .../wizard/account_report_partner_balance.py | 47 - .../account_report_partner_balance_view.xml | 38 - .../wizard/account_report_partner_ledger.py | 62 - .../account_report_partner_ledger_view.xml | 51 - .../wizard/account_report_print_journal.py | 72 - .../account_report_print_journal_view.xml | 66 - addons/account/wizard/account_state_open.py | 41 - .../wizard/account_state_open_view.xml | 34 - .../wizard/account_statement_from_invoice.py | 85 - .../account_statement_from_invoice_view.xml | 33 - .../wizard/account_subscription_generate.py | 49 - .../account_subscription_generate_view.xml | 34 - addons/account/wizard/account_tax_chart.py | 74 - .../account/wizard/account_tax_chart_view.xml | 46 - addons/account/wizard/account_unreconcile.py | 47 +- .../wizard/account_unreconcile_view.xml | 34 - addons/account/wizard/account_use_model.py | 73 - .../account/wizard/account_use_model_view.xml | 47 - .../wizard/account_validate_account_move.py | 71 +- .../wizard/account_validate_move_view.xml | 56 +- addons/account/wizard/account_vat.py | 64 - addons/account/wizard/account_vat_view.xml | 48 - addons/account/wizard/pos_box.py | 84 +- addons/account/wizard/pos_box.xml | 2 +- addons/account_accountant/__openerp__.py | 10 +- .../account_accountant_data.xml | 12 +- .../account_analytic_analysis/__openerp__.py | 4 +- .../account_analytic_analysis.py | 28 +- .../account_analytic_analysis_view.xml | 8 +- .../analytic_account_demo.xml | 6 - .../analytic_account_demo.yml | 15 + .../security/ir.model.access.csv | 1 - .../test/account_analytic_analysis.yml | 2 +- .../account_analytic_default.py | 17 +- .../account_analytic_plans.py | 37 +- .../account_analytic_plans_view.xml | 7 +- addons/account_anglo_saxon/__init__.py | 24 - addons/account_anglo_saxon/__openerp__.py | 49 - .../i18n/account_anglo_saxon.pot | 74 - addons/account_anglo_saxon/i18n/ar.po | 74 - addons/account_anglo_saxon/i18n/bg.po | 76 - addons/account_anglo_saxon/i18n/bs.po | 76 - addons/account_anglo_saxon/i18n/ca.po | 76 - addons/account_anglo_saxon/i18n/cs.po | 75 - addons/account_anglo_saxon/i18n/da.po | 75 - addons/account_anglo_saxon/i18n/de.po | 77 - addons/account_anglo_saxon/i18n/el.po | 76 - addons/account_anglo_saxon/i18n/en_GB.po | 78 - addons/account_anglo_saxon/i18n/es.po | 78 - addons/account_anglo_saxon/i18n/es_AR.po | 76 - addons/account_anglo_saxon/i18n/es_CR.po | 76 - addons/account_anglo_saxon/i18n/es_EC.po | 76 - addons/account_anglo_saxon/i18n/es_MX.po | 76 - addons/account_anglo_saxon/i18n/es_PY.po | 76 - addons/account_anglo_saxon/i18n/es_VE.po | 162 - addons/account_anglo_saxon/i18n/et.po | 74 - addons/account_anglo_saxon/i18n/fa.po | 74 - addons/account_anglo_saxon/i18n/fi.po | 76 - addons/account_anglo_saxon/i18n/fr.po | 76 - addons/account_anglo_saxon/i18n/gl.po | 76 - addons/account_anglo_saxon/i18n/gu.po | 74 - addons/account_anglo_saxon/i18n/hi.po | 75 - addons/account_anglo_saxon/i18n/hr.po | 76 - addons/account_anglo_saxon/i18n/hu.po | 78 - addons/account_anglo_saxon/i18n/id.po | 74 - addons/account_anglo_saxon/i18n/it.po | 78 - addons/account_anglo_saxon/i18n/ja.po | 74 - addons/account_anglo_saxon/i18n/lv.po | 76 - addons/account_anglo_saxon/i18n/mk.po | 76 - addons/account_anglo_saxon/i18n/mn.po | 76 - addons/account_anglo_saxon/i18n/nb.po | 76 - addons/account_anglo_saxon/i18n/nl.po | 78 - addons/account_anglo_saxon/i18n/nl_BE.po | 78 - addons/account_anglo_saxon/i18n/oc.po | 74 - addons/account_anglo_saxon/i18n/pl.po | 76 - addons/account_anglo_saxon/i18n/pt.po | 78 - addons/account_anglo_saxon/i18n/pt_BR.po | 79 - addons/account_anglo_saxon/i18n/ro.po | 78 - addons/account_anglo_saxon/i18n/ru.po | 76 - addons/account_anglo_saxon/i18n/sk.po | 74 - addons/account_anglo_saxon/i18n/sl.po | 74 - addons/account_anglo_saxon/i18n/sq.po | 74 - addons/account_anglo_saxon/i18n/sr@latin.po | 76 - addons/account_anglo_saxon/i18n/sv.po | 76 - addons/account_anglo_saxon/i18n/ta.po | 74 - addons/account_anglo_saxon/i18n/tr.po | 78 - addons/account_anglo_saxon/i18n/zh_CN.po | 74 - addons/account_anglo_saxon/i18n/zh_TW.po | 74 - addons/account_anglo_saxon/invoice.py | 211 - addons/account_anglo_saxon/product.py | 67 - addons/account_anglo_saxon/product_view.xml | 32 - addons/account_anglo_saxon/purchase.py | 38 - addons/account_anglo_saxon/stock.py | 58 - addons/account_asset/__openerp__.py | 6 +- addons/account_asset/account_asset.py | 65 +- addons/account_asset/account_asset_demo.yml | 73 + addons/account_asset/account_asset_invoice.py | 8 +- .../account_asset_invoice_view.xml | 2 +- addons/account_asset/account_asset_view.xml | 24 +- .../report/account_asset_report_view.xml | 2 +- addons/account_asset/test/account_asset.yml | 14 +- .../account_asset/test/account_asset_demo.yml | 52 +- .../account_asset_demo_test.xml} | 46 +- .../test/account_asset_wizard.yml | 8 +- .../wizard/wizard_asset_compute.py | 16 +- .../wizard/wizard_asset_compute_view.xml | 15 +- .../__init__.py | 26 - .../__openerp__.py | 57 - .../account_bank_statement.py | 135 - .../account_bank_statement_report.xml | 13 - .../account_bank_statement_view.xml | 146 - ...account_bank_statement_extensions_data.xml | 15 - .../account_bank_statement_extensions.pot | 388 -- .../i18n/ar.po | 389 -- .../i18n/bg.po | 388 -- .../i18n/bs.po | 394 -- .../i18n/ca.po | 388 -- .../i18n/cs.po | 388 -- .../i18n/da.po | 388 -- .../i18n/de.po | 393 -- .../i18n/en_GB.po | 392 -- .../i18n/es.po | 396 -- .../i18n/es_AR.po | 388 -- .../i18n/es_CR.po | 393 -- .../i18n/es_EC.po | 388 -- .../i18n/es_MX.po | 396 -- .../i18n/fi.po | 388 -- .../i18n/fr.po | 395 -- .../i18n/gu.po | 388 -- .../i18n/hr.po | 388 -- .../i18n/hu.po | 393 -- .../i18n/it.po | 396 -- .../i18n/ja.po | 388 -- .../i18n/lv.po | 392 -- .../i18n/mk.po | 396 -- .../i18n/mn.po | 392 -- .../i18n/nb.po | 392 -- .../i18n/nl.po | 394 -- .../i18n/pl.po | 391 -- .../i18n/pt.po | 396 -- .../i18n/pt_BR.po | 396 -- .../i18n/ro.po | 395 -- .../i18n/ru.po | 392 -- .../i18n/sl.po | 388 -- .../i18n/sr@latin.po | 390 -- .../i18n/sv.po | 391 -- .../i18n/tr.po | 393 -- .../i18n/zh_CN.po | 388 -- .../i18n/zh_TW.po | 388 -- .../report/__init__.py | 24 - .../report/bank_statement_balance_report.py | 62 - .../res_partner_bank.py | 36 - .../security/ir.model.access.csv | 3 - .../views/report_bankstatementbalance.xml | 35 - .../wizard/__init__.py | 24 - .../wizard/cancel_statement_line.py | 33 - .../wizard/cancel_statement_line_wizard.xml | 38 - .../wizard/confirm_statement_line.py | 33 - .../wizard/confirm_statement_line_wizard.xml | 40 - .../__openerp__.py | 12 +- .../account_bank_statement_import.py | 168 +- .../account_bank_statement_import_view.xml | 8 +- .../demo/fiscalyear_period.xml | 119 - .../__openerp__.py | 19 +- .../account_bank_statement_import_ofx.py | 22 +- .../tests/test_import_bank_statement.py | 24 +- .../__openerp__.py | 7 +- .../account_bank_statement_import_qif.py | 51 +- .../tests/test_import_bank_statement.py | 22 +- addons/account_budget/__openerp__.py | 4 +- addons/account_budget/account_budget.py | 8 +- addons/account_budget/account_budget_demo.xml | 165 - addons/account_budget/account_budget_demo.yml | 175 + addons/account_budget/account_budget_view.xml | 53 +- addons/account_budget/test/account_budget.yml | 22 + .../tests/test_theoreticalamount.py | 30 +- addons/account_cancel/account_cancel_view.xml | 15 +- addons/account_check_writing/__openerp__.py | 3 +- addons/account_check_writing/account_demo.xml | 4 - addons/account_check_writing/account_view.xml | 2 +- .../account_voucher_view.xml | 65 - addons/account_followup/__init__.py | 24 - addons/account_followup/__openerp__.py | 67 - addons/account_followup/account_followup.py | 503 --- .../account_followup_customers.xml | 163 - .../account_followup_data.xml | 226 - .../account_followup_demo.xml | 48 - .../account_followup_reports.xml | 14 - .../account_followup_view.xml | 176 - .../i18n/account_followup.pot | 1216 ------ addons/account_followup/i18n/ar.po | 1302 ------ addons/account_followup/i18n/bg.po | 1297 ------ addons/account_followup/i18n/bs.po | 1397 ------ addons/account_followup/i18n/ca.po | 1303 ------ addons/account_followup/i18n/cs.po | 1297 ------ addons/account_followup/i18n/da.po | 1297 ------ addons/account_followup/i18n/de.po | 1566 ------- addons/account_followup/i18n/el.po | 1489 ------- addons/account_followup/i18n/en_GB.po | 1543 ------- addons/account_followup/i18n/es.po | 1802 -------- addons/account_followup/i18n/es_AR.po | 1558 ------- addons/account_followup/i18n/es_CR.po | 1305 ------ addons/account_followup/i18n/es_EC.po | 1299 ------ addons/account_followup/i18n/es_MX.po | 1015 ----- addons/account_followup/i18n/es_PY.po | 1303 ------ addons/account_followup/i18n/es_VE.po | 1015 ----- addons/account_followup/i18n/et.po | 1297 ------ addons/account_followup/i18n/fa.po | 1297 ------ addons/account_followup/i18n/fi.po | 1572 ------- addons/account_followup/i18n/fr.po | 1532 ------- addons/account_followup/i18n/gl.po | 1302 ------ addons/account_followup/i18n/hr.po | 1301 ------ addons/account_followup/i18n/hu.po | 1557 ------- addons/account_followup/i18n/id.po | 1297 ------ addons/account_followup/i18n/it.po | 1460 ------- addons/account_followup/i18n/ja.po | 1297 ------ addons/account_followup/i18n/ko.po | 1412 ------ addons/account_followup/i18n/lt.po | 1297 ------ addons/account_followup/i18n/lv.po | 1299 ------ addons/account_followup/i18n/mk.po | 1452 ------- addons/account_followup/i18n/mn.po | 1537 ------- addons/account_followup/i18n/nb.po | 1310 ------ addons/account_followup/i18n/nl.po | 1565 ------- addons/account_followup/i18n/nl_BE.po | 1559 ------- addons/account_followup/i18n/oc.po | 1297 ------ addons/account_followup/i18n/pl.po | 1310 ------ addons/account_followup/i18n/pt.po | 1556 ------- addons/account_followup/i18n/pt_BR.po | 1553 ------- addons/account_followup/i18n/ro.po | 1562 ------- addons/account_followup/i18n/ru.po | 1301 ------ addons/account_followup/i18n/sl.po | 1490 ------- addons/account_followup/i18n/sq.po | 1297 ------ addons/account_followup/i18n/sr.po | 1301 ------ addons/account_followup/i18n/sr@latin.po | 1300 ------ addons/account_followup/i18n/sv.po | 1302 ------ addons/account_followup/i18n/tlh.po | 1171 ----- addons/account_followup/i18n/tr.po | 1539 ------- addons/account_followup/i18n/uk.po | 1297 ------ addons/account_followup/i18n/vi.po | 1297 ------ addons/account_followup/i18n/zh_CN.po | 1325 ------ addons/account_followup/i18n/zh_TW.po | 1299 ------ addons/account_followup/report/__init__.py | 23 - .../report/account_followup_print.py | 120 - .../report/account_followup_report.py | 93 - .../report/account_followup_report.xml | 65 - addons/account_followup/res_config_view.xml | 15 - .../security/account_followup_security.xml | 20 - .../security/ir.model.access.csv | 7 - .../test/account_followup.yml | 25 - addons/account_followup/tests/__init__.py | 22 - .../tests/test_account_followup.py | 149 - .../views/report_followup.xml | 59 - addons/account_followup/wizard/__init__.py | 22 - .../wizard/account_followup_print.py | 320 -- .../wizard/account_followup_print_view.xml | 92 - addons/account_payment/__init__.py | 30 - addons/account_payment/__openerp__.py | 68 - addons/account_payment/account_invoice.py | 44 - addons/account_payment/account_move_line.py | 104 - addons/account_payment/account_payment.py | 406 -- .../account_payment/account_payment_demo.xml | 25 - .../account_payment_report.xml | 13 - .../account_payment_sequence.xml | 19 - .../account_payment/account_payment_view.xml | 297 -- .../account_payment_workflow.xml | 63 - .../account_payment/i18n/account_payment.pot | 719 --- addons/account_payment/i18n/am.po | 726 ---- addons/account_payment/i18n/ar.po | 730 ---- addons/account_payment/i18n/bg.po | 726 ---- addons/account_payment/i18n/bs.po | 744 ---- addons/account_payment/i18n/ca.po | 735 ---- addons/account_payment/i18n/cs.po | 726 ---- addons/account_payment/i18n/da.po | 726 ---- addons/account_payment/i18n/de.po | 751 ---- addons/account_payment/i18n/el.po | 738 ---- addons/account_payment/i18n/en_GB.po | 748 ---- addons/account_payment/i18n/es.po | 749 ---- addons/account_payment/i18n/es_AR.po | 733 ---- addons/account_payment/i18n/es_CL.po | 726 ---- addons/account_payment/i18n/es_CR.po | 736 ---- addons/account_payment/i18n/es_EC.po | 736 ---- addons/account_payment/i18n/es_MX.po | 726 ---- addons/account_payment/i18n/es_PY.po | 736 ---- addons/account_payment/i18n/es_VE.po | 810 ---- addons/account_payment/i18n/et.po | 730 ---- addons/account_payment/i18n/fa.po | 726 ---- addons/account_payment/i18n/fi.po | 734 ---- addons/account_payment/i18n/fr.po | 752 ---- addons/account_payment/i18n/gl.po | 734 ---- addons/account_payment/i18n/hi.po | 728 ---- addons/account_payment/i18n/hr.po | 735 ---- addons/account_payment/i18n/hu.po | 748 ---- addons/account_payment/i18n/id.po | 734 ---- addons/account_payment/i18n/it.po | 748 ---- addons/account_payment/i18n/ja.po | 727 ---- addons/account_payment/i18n/ko.po | 728 ---- addons/account_payment/i18n/lt.po | 726 ---- addons/account_payment/i18n/lv.po | 731 ---- addons/account_payment/i18n/mk.po | 746 ---- addons/account_payment/i18n/mn.po | 745 ---- addons/account_payment/i18n/nb.po | 731 ---- addons/account_payment/i18n/nl.po | 747 ---- addons/account_payment/i18n/nl_BE.po | 726 ---- addons/account_payment/i18n/oc.po | 726 ---- addons/account_payment/i18n/pl.po | 747 ---- addons/account_payment/i18n/pt.po | 750 ---- addons/account_payment/i18n/pt_BR.po | 749 ---- addons/account_payment/i18n/ro.po | 749 ---- addons/account_payment/i18n/ru.po | 733 ---- addons/account_payment/i18n/sl.po | 739 ---- addons/account_payment/i18n/sq.po | 726 ---- addons/account_payment/i18n/sr.po | 733 ---- addons/account_payment/i18n/sr@latin.po | 733 ---- addons/account_payment/i18n/sv.po | 747 ---- addons/account_payment/i18n/tlh.po | 726 ---- addons/account_payment/i18n/tr.po | 747 ---- addons/account_payment/i18n/uk.po | 726 ---- addons/account_payment/i18n/vi.po | 726 ---- addons/account_payment/i18n/zh_CN.po | 735 ---- addons/account_payment/i18n/zh_TW.po | 732 ---- addons/account_payment/report/__init__.py | 22 - .../account_payment/report/payment_order.py | 79 - .../security/account_payment_security.xml | 38 - .../security/ir.model.access.csv | 8 - .../test/account_payment_demo.yml | 13 - .../test/account_payment_report.yml | 10 - .../test/cancel_payment_order.yml | 30 - .../test/payment_order_process.yml | 111 - .../views/report_paymentorder.xml | 109 - addons/account_payment/wizard/__init__.py | 24 - .../account_payment_create_order_view.xml | 50 - .../wizard/account_payment_order.py | 121 - .../wizard/account_payment_pay.py | 55 - .../wizard/account_payment_pay_view.xml | 32 - .../account_payment_populate_statement.py | 89 - ...ccount_payment_populate_statement_view.xml | 45 - addons/account_sequence/__init__.py | 23 - addons/account_sequence/__openerp__.py | 51 - addons/account_sequence/account_sequence.py | 59 - addons/account_sequence/account_sequence.xml | 78 - .../account_sequence_data.xml | 16 - .../account_sequence_installer.py | 80 - .../account_sequence_installer_view.xml | 53 - .../i18n/account_sequence.pot | 165 - addons/account_sequence/i18n/ar.po | 172 - addons/account_sequence/i18n/bg.po | 170 - addons/account_sequence/i18n/bs.po | 173 - addons/account_sequence/i18n/ca.po | 175 - addons/account_sequence/i18n/cs.po | 170 - addons/account_sequence/i18n/da.po | 170 - addons/account_sequence/i18n/de.po | 175 - addons/account_sequence/i18n/el.po | 170 - addons/account_sequence/i18n/en_GB.po | 174 - addons/account_sequence/i18n/es.po | 176 - addons/account_sequence/i18n/es_AR.po | 173 - addons/account_sequence/i18n/es_CR.po | 174 - addons/account_sequence/i18n/es_EC.po | 173 - addons/account_sequence/i18n/es_MX.po | 235 - addons/account_sequence/i18n/es_PY.po | 174 - addons/account_sequence/i18n/es_VE.po | 235 - addons/account_sequence/i18n/fa.po | 170 - addons/account_sequence/i18n/fr.po | 174 - addons/account_sequence/i18n/gl.po | 173 - addons/account_sequence/i18n/hr.po | 170 - addons/account_sequence/i18n/hu.po | 174 - addons/account_sequence/i18n/id.po | 170 - addons/account_sequence/i18n/it.po | 174 - addons/account_sequence/i18n/ja.po | 170 - addons/account_sequence/i18n/lv.po | 172 - addons/account_sequence/i18n/mk.po | 174 - addons/account_sequence/i18n/mn.po | 173 - addons/account_sequence/i18n/nb.po | 172 - addons/account_sequence/i18n/nl.po | 175 - addons/account_sequence/i18n/nl_BE.po | 174 - addons/account_sequence/i18n/pl.po | 174 - addons/account_sequence/i18n/pt.po | 174 - addons/account_sequence/i18n/pt_BR.po | 175 - addons/account_sequence/i18n/ro.po | 172 - addons/account_sequence/i18n/ru.po | 172 - addons/account_sequence/i18n/sl.po | 170 - addons/account_sequence/i18n/sq.po | 170 - addons/account_sequence/i18n/sr@latin.po | 173 - addons/account_sequence/i18n/sv.po | 173 - addons/account_sequence/i18n/tr.po | 173 - addons/account_sequence/i18n/vi.po | 170 - addons/account_sequence/i18n/zh_CN.po | 170 - addons/account_sequence/i18n/zh_TW.po | 170 - addons/account_tax_python/__init__.py | 2 + addons/account_tax_python/__openerp__.py | 20 + .../account_tax_python/account_tax_python.py | 78 + .../account_tax_python/account_tax_python.xml | 22 + addons/account_test/__init__.py | 20 + addons/account_test/__openerp__.py | 4 +- addons/account_test/account_test.py | 54 +- addons/account_test/account_test_data.xml | 76 +- addons/account_test/account_test_report.xml | 6 +- addons/account_test/account_test_view.xml | 14 +- addons/account_test/report/__init__.py | 1 + .../report/account_test_report.py | 76 +- .../account_test/views/report_accounttest.xml | 6 +- addons/account_voucher/__init__.py | 1 - addons/account_voucher/__openerp__.py | 27 +- addons/account_voucher/account_voucher.py | 1671 ++----- .../account_voucher_pay_invoice.xml | 28 - .../account_voucher_sequence.xml | 57 - .../account_voucher/account_voucher_view.xml | 160 +- .../account_voucher_wizard.xml | 5 - .../account_voucher_workflow.xml | 84 - addons/account_voucher/invoice.py | 54 - .../report/account_voucher_sales_receipt.py | 76 +- .../account_voucher_sales_receipt_view.xml | 2 +- .../static/description/account_illu_01.png | Bin 27792 -> 0 bytes .../static/description/account_sc_00.png | Bin 80153 -> 0 bytes .../static/description/account_sc_02.png | Bin 75829 -> 0 bytes .../static/description/account_sc_03.png | Bin 28092 -> 0 bytes .../static/description/account_sc_04.png | Bin 37443 -> 0 bytes .../static/description/account_sc_06.png | Bin 122701 -> 0 bytes .../static/description/index.html | 140 - .../static/description/testimonial_sunil.jpg | Bin 2988 -> 0 bytes .../account_voucher/test/account_voucher.yml | 96 +- .../test/account_voucher_chart.yml | 47 + addons/account_voucher/test/case1_usd_usd.yml | 360 -- .../test/case1_usd_usd_payment_rate.yml | 253 -- .../test/case2_suppl_usd_eur.yml | 328 -- .../test/case2_usd_eur_debtor_in_eur.yml | 326 -- .../test/case2_usd_eur_debtor_in_usd.yml | 336 -- addons/account_voucher/test/case3_eur_eur.yml | 287 -- addons/account_voucher/test/case4_cad_chf.yml | 199 - .../test/case5_suppl_usd_usd.yml | 189 - addons/account_voucher/test/case_eur_usd.yml | 176 - addons/account_voucher/test/sales_payment.yml | 77 - addons/account_voucher/test/sales_receipt.yml | 125 +- .../voucher_payment_receipt_view.xml | 581 --- .../voucher_sales_purchase_view.xml | 232 +- addons/analytic/__openerp__.py | 1 - addons/analytic/analytic_report.xml | 10 - addons/analytic/models/analytic.py | 8 +- addons/analytic/views/analytic_view.xml | 14 +- addons/analytic/wizard/__init__.py | 1 - .../wizard/account_analytic_journal_report.py | 72 - .../account_analytic_journal_report_view.xml | 44 - .../analytic_contract_hr_expense.py | 2 +- addons/auth_oauth/auth_oauth_data.xml | 9 + .../report/crm_partner_report.py | 4 +- addons/delivery/__openerp__.py | 5 +- addons/delivery/sale.py | 2 +- addons/delivery/stock.py | 6 +- addons/delivery/test/delivery_cost.yml | 3 +- addons/edi/__init__.py | 24 - addons/edi/__openerp__.py | 45 - addons/edi/controllers/__init__.py | 1 - addons/edi/controllers/main.py | 25 - addons/edi/edi_service.py | 61 - addons/edi/i18n/ar.po | 92 - addons/edi/i18n/bg.po | 92 - addons/edi/i18n/cs.po | 92 - addons/edi/i18n/de.po | 96 - addons/edi/i18n/edi.pot | 88 - addons/edi/i18n/en_GB.po | 95 - addons/edi/i18n/es.po | 95 - addons/edi/i18n/es_AR.po | 92 - addons/edi/i18n/es_CR.po | 92 - addons/edi/i18n/fi.po | 92 - addons/edi/i18n/fr.po | 92 - addons/edi/i18n/he.po | 92 - addons/edi/i18n/hr.po | 92 - addons/edi/i18n/hu.po | 94 - addons/edi/i18n/is.po | 92 - addons/edi/i18n/it.po | 95 - addons/edi/i18n/ja.po | 92 - addons/edi/i18n/lt.po | 92 - addons/edi/i18n/mk.po | 95 - addons/edi/i18n/ml.po | 92 - addons/edi/i18n/mn.po | 92 - addons/edi/i18n/nb.po | 92 - addons/edi/i18n/nl.po | 95 - addons/edi/i18n/nl_BE.po | 92 - addons/edi/i18n/pl.po | 95 - addons/edi/i18n/pt.po | 95 - addons/edi/i18n/pt_BR.po | 96 - addons/edi/i18n/ro.po | 92 - addons/edi/i18n/ru.po | 92 - addons/edi/i18n/sl.po | 92 - addons/edi/i18n/sv.po | 92 - addons/edi/i18n/ta.po | 92 - addons/edi/i18n/tr.po | 95 - addons/edi/i18n/zh_CN.po | 92 - addons/edi/i18n/zh_TW.po | 92 - addons/edi/models/__init__.py | 25 - addons/edi/models/edi.py | 604 --- addons/edi/models/res_company.py | 57 - addons/edi/models/res_currency.py | 64 - addons/edi/models/res_partner.py | 90 - addons/edi/static/src/img/knowledge.png | Bin 948 -> 0 bytes addons/edi/static/src/js/edi.js | 77 - addons/edi/test/edi_partner_test.yml | 42 - addons/edi/views/edi.xml | 10 - addons/event_sale/models/sale_order.py | 4 +- addons/event_sale/test/confirm.yml | 2 +- addons/hr_expense/__openerp__.py | 1 + addons/hr_expense/hr_expense.py | 91 +- addons/hr_expense/hr_expense_view.xml | 14 +- addons/hr_expense/report/hr_expense_report.py | 4 +- .../hr_expense/security/ir.model.access.csv | 3 - .../hr_expense/tests/test_journal_entries.py | 31 +- addons/hr_payroll_account/__openerp__.py | 2 +- .../hr_payroll_account/hr_payroll_account.py | 76 +- .../hr_payroll_account_demo.xml | 25 - .../hr_payroll_account_view.xml | 2 +- .../test/hr_payroll_account.yml | 4 +- .../report/hr_recruitment_report.py | 8 +- addons/hr_timesheet/__openerp__.py | 7 +- addons/hr_timesheet/hr_timesheet_demo.xml | 70 - addons/hr_timesheet/hr_timesheet_demo.yml | 72 + .../report/hr_timesheet_report.py | 2 +- .../hr_timesheet/security/ir.model.access.csv | 3 +- .../hr_timesheet/test/hr_timesheet_data.xml | 18 + addons/hr_timesheet_invoice/__openerp__.py | 3 +- .../hr_timesheet_invoice.py | 12 +- .../report/account_analytic_profit.py | 4 +- .../report/hr_timesheet_invoice_report.py | 2 +- .../report/report_analytic.py | 2 +- .../report/report_analytic_view.xml | 4 +- .../test/test_hr_timesheet_invoice.yml | 23 +- .../test_hr_timesheet_invoice_no_prod_tax.yml | 19 +- .../tests/test_invoice.py | 22 +- addons/hr_timesheet_sheet/__openerp__.py | 6 +- .../test/test_hr_timesheet_sheet.yml | 4 +- addons/inter_company_rules/__openerp__.py | 9 +- .../models/account_invoice.py | 55 +- .../models/purchase_order.py | 2 +- .../inter_company_rules/models/res_company.py | 2 +- .../inter_company_rules/models/sale_order.py | 2 +- .../test/inter_company_invoice.yml | 20 +- .../test/test_intercompany_data.yml | 16 +- .../marketing_campaign/marketing_campaign.py | 4 +- .../marketing_campaign_view.xml | 2 +- .../report/campaign_analysis.py | 4 +- .../report/campaign_analysis_view.xml | 4 +- addons/membership/__openerp__.py | 7 +- addons/membership/membership.py | 42 +- addons/membership/membership_demo.xml | 46 - addons/membership/membership_demo.yml | 54 +- addons/membership/membership_view.xml | 3 +- addons/membership/report/report_membership.py | 4 +- addons/mrp/mrp.py | 2 +- addons/mrp/mrp_demo.xml | 2 - addons/mrp/mrp_demo.yml | 8 + addons/mrp_repair/mrp_repair.py | 18 +- addons/mrp_repair/mrp_repair_view.xml | 12 +- addons/multi_company/__init__.py | 20 - addons/multi_company/__openerp__.py | 47 - addons/multi_company/i18n/ar.po | 59 - addons/multi_company/i18n/bg.po | 59 - addons/multi_company/i18n/bs.po | 59 - addons/multi_company/i18n/ca.po | 59 - addons/multi_company/i18n/cs.po | 69 - addons/multi_company/i18n/da.po | 59 - addons/multi_company/i18n/de.po | 72 - addons/multi_company/i18n/es.po | 69 - addons/multi_company/i18n/es_AR.po | 69 - addons/multi_company/i18n/es_CR.po | 59 - addons/multi_company/i18n/es_EC.po | 59 - addons/multi_company/i18n/es_MX.po | 112 - addons/multi_company/i18n/es_VE.po | 112 - addons/multi_company/i18n/et.po | 59 - addons/multi_company/i18n/fi.po | 72 - addons/multi_company/i18n/fr.po | 72 - addons/multi_company/i18n/gl.po | 70 - addons/multi_company/i18n/hr.po | 69 - addons/multi_company/i18n/hu.po | 75 - addons/multi_company/i18n/id.po | 59 - addons/multi_company/i18n/it.po | 72 - addons/multi_company/i18n/ja.po | 59 - addons/multi_company/i18n/lo.po | 59 - addons/multi_company/i18n/lt.po | 59 - addons/multi_company/i18n/lv.po | 59 - addons/multi_company/i18n/mk.po | 70 - addons/multi_company/i18n/mn.po | 67 - addons/multi_company/i18n/multi_company.pot | 55 - addons/multi_company/i18n/nb.po | 59 - addons/multi_company/i18n/nl.po | 70 - addons/multi_company/i18n/oc.po | 59 - addons/multi_company/i18n/pl.po | 59 - addons/multi_company/i18n/pt.po | 69 - addons/multi_company/i18n/pt_BR.po | 72 - addons/multi_company/i18n/ro.po | 70 - addons/multi_company/i18n/ru.po | 69 - addons/multi_company/i18n/sl.po | 59 - addons/multi_company/i18n/sr.po | 59 - addons/multi_company/i18n/sr@latin.po | 59 - addons/multi_company/i18n/sv.po | 68 - addons/multi_company/i18n/tr.po | 70 - addons/multi_company/i18n/uk.po | 59 - addons/multi_company/i18n/vi.po | 59 - addons/multi_company/i18n/zh_CN.po | 67 - addons/multi_company/i18n/zh_TW.po | 59 - addons/multi_company/multi_company_demo.xml | 561 --- addons/multi_company/res_company_view.xml | 71 - .../security/ir.model.access.csv | 1 - addons/payment/models/payment_acquirer.py | 2 +- addons/point_of_sale/__openerp__.py | 7 +- .../point_of_sale/account_bank_statement.py | 3 +- .../point_of_sale/account_statement_demo.xml | 18 - .../point_of_sale/account_statement_view.xml | 93 - addons/point_of_sale/point_of_sale.py | 221 +- addons/point_of_sale/point_of_sale_demo.xml | 7 - addons/point_of_sale/point_of_sale_view.xml | 61 +- addons/point_of_sale/report/pos_details.py | 10 +- .../security/ir.model.access.csv | 9 - addons/point_of_sale/static/src/js/models.js | 26 +- .../test/01_order_to_payment.yml | 18 +- .../test/account_statement_reports.yml | 19 +- addons/point_of_sale/test/tests_before.xml | 13 + .../point_of_sale/views/report_statement.xml | 4 +- .../views/report_usersproduct.xml | 4 +- .../wizard/pos_open_statement.py | 12 +- addons/portal_sale/portal_sale_view.xml | 10 - addons/product/product.py | 2 +- addons/product/product_data.xml | 4 - .../product_email_template/models/invoice.py | 2 +- addons/product_margin/product_margin.py | 8 +- addons/product_margin/product_margin_view.xml | 2 +- .../product_visible_discount.py | 4 +- .../security/ir.model.access.csv | 2 - .../worktask_entry_to_timesheetline_entry.yml | 8 +- addons/purchase/__init__.py | 1 - addons/purchase/__openerp__.py | 10 +- addons/purchase/edi/__init__.py | 22 - addons/purchase/edi/purchase_order.py | 192 - .../edi/purchase_order_action_data.xml | 167 - addons/purchase/partner.py | 2 +- addons/purchase/partner_view.xml | 12 +- addons/purchase/purchase.py | 157 +- addons/purchase/purchase_data.xml | 2 +- addons/purchase/purchase_demo.xml | 3 - addons/purchase/purchase_order_demo.yml | 7 +- addons/purchase/purchase_view.xml | 67 +- addons/purchase/report/purchase_report.py | 6 +- .../purchase/report/purchase_report_view.xml | 2 +- addons/purchase/res_config.py | 6 +- addons/purchase/security/ir.model.access.csv | 6 +- addons/purchase/stock.py | 6 +- addons/purchase/stock_view.xml | 4 +- .../test/anglo_saxon.yml | 143 +- .../test/anglo_saxon_avg_fifo.yml | 157 +- addons/purchase/test/average_price.yml | 16 +- addons/purchase/test/fifo_price.yml | 30 +- addons/purchase/test/fifo_returns.yml | 8 +- .../test/process/edi_purchase_order.yml | 240 - .../purchase/test/process/rfq2order2done.yml | 3 +- .../test/stock_valuation_account.xml} | 3 +- .../tests/test_purchase_to_invoice.py | 17 +- .../purchase/wizard/purchase_line_invoice.py | 8 +- .../wizard/purchase_line_invoice_view.xml | 2 +- .../purchase_requisition.py | 2 +- addons/report/models/report.py | 3 +- addons/report_intrastat/__openerp__.py | 2 +- addons/report_intrastat/report_intrastat.py | 2 +- .../report_intrastat_view.xml | 2 +- .../test/report_intrastat_report.yml | 2 +- .../views/report_intrastatinvoice.xml | 20 +- addons/sale/__init__.py | 1 - addons/sale/__openerp__.py | 5 +- addons/sale/edi/__init__.py | 22 - addons/sale/edi/sale_order.py | 225 - addons/sale/edi/sale_order_action_data.xml | 105 - addons/sale/sale.py | 80 +- addons/sale/sale_demo.xml | 89 +- addons/sale/sale_demo.yml | 77 + addons/sale/sale_unit_test.xml | 6 - addons/sale/sale_view.xml | 33 +- addons/sale/security/ir.model.access.csv | 3 - addons/sale/test/cancel_order.yml | 2 +- addons/sale/test/edi_sale_order.yml | 217 - addons/sale/test/manual_order_policy.yml | 7 +- addons/sale/test/sale_order_demo.yml | 2 +- addons/sale/tests/test_sale_to_invoice.py | 44 +- addons/sale/views/report_saleorder.xml | 14 +- addons/sale/wizard/sale_line_invoice.py | 6 +- .../sale/wizard/sale_make_invoice_advance.py | 35 +- .../sale/wizard/sale_make_invoice_advance.xml | 2 +- addons/sale_crm/crm_lead.py | 2 +- addons/sale_layout/models/sale_layout.py | 2 +- .../views/report_invoice_layouted.xml | 2 +- .../views/sale_layout_category_view.xml | 2 +- addons/sale_margin/sale_margin.py | 4 +- addons/sale_stock/__openerp__.py | 1 + addons/sale_stock/sale_stock.py | 10 +- addons/sale_stock/sale_stock_demo.xml | 6 - addons/sale_stock/sale_stock_demo.yml | 13 + addons/sale_stock/sale_stock_view.xml | 8 +- .../sale_stock/security/ir.model.access.csv | 4 +- .../sale_stock/test/picking_order_policy.yml | 13 +- addons/stock_account/__openerp__.py | 3 +- addons/stock_account/product.py | 58 +- addons/stock_account/product_data.xml | 7 - addons/stock_account/product_view.xml | 10 +- .../security/ir.model.access.csv | 1 - addons/stock_account/stock.py | 27 +- addons/stock_account/stock_account.py | 183 +- addons/stock_account/stock_account_data.xml | 12 - addons/stock_account/stock_account_demo.yml | 16 + .../stock_account_post_install.yml | 21 + addons/stock_account/stock_account_view.xml | 2 +- .../test/stock_valuation_account.xml | 15 + .../wizard/stock_invoice_onshipping.py | 54 +- .../wizard/stock_invoice_onshipping_view.xml | 4 +- addons/stock_dropshipping/__openerp__.py | 2 + addons/stock_dropshipping/test/dropship.yml | 2 +- addons/stock_dropshipping/test/lifo_price.yml | 6 +- .../test/procurementexception.yml | 2 +- .../tests/test_invoicing.py | 12 + .../wizard/stock_invoice_onshipping.py | 18 +- addons/stock_invoice_directly/__openerp__.py | 2 +- .../test/stock_invoice_directly.yml | 2 +- addons/stock_landed_costs/__openerp__.py | 4 +- .../stock_landed_costs/stock_landed_costs.py | 29 +- .../stock_landed_costs_view.xml | 4 +- .../test/stock_landed_costs.yml | 14 +- addons/warning/warning.py | 8 +- addons/web/static/lib/py.js/lib/py.js | 7 +- .../static/lib/zeroclipboard/ZeroClipboard.js | 0 .../lib/zeroclipboard/ZeroClipboard.swf | Bin addons/web/static/test/evals.js | 31 +- .../static/src/js/website_links.js | 2 +- .../static/src/js/website_links_charts.js | 2 +- .../views/website_links_template.xml | 2 +- addons/website_quote/controllers/main.py | 2 +- addons/website_quote/models/order.py | 7 +- .../website_quote/views/website_quotation.xml | 2 +- .../views/website_quotation_backend.xml | 4 +- addons/website_sale/controllers/main.py | 4 +- addons/website_sale/models/sale_order.py | 10 +- .../models/sale_order.py | 2 +- doc/api_integration.rst | 8 +- openerp/addons/base/__openerp__.py | 1 + openerp/addons/base/base_data.xml | 13 - openerp/addons/base/base_demo.xml | 13 +- openerp/addons/base/i18n/base.pot | 4 +- openerp/addons/base/ir/ir_qweb.py | 1 - openerp/addons/base/module/module_data.xml | 2 +- openerp/addons/base/res/res_bank.py | 11 +- openerp/addons/base/res/res_bank_view.xml | 26 +- openerp/addons/base/res/res_company.py | 3 +- openerp/addons/base/res/res_currency.py | 90 +- openerp/addons/base/res/res_currency_data.xml | 1046 +---- openerp/addons/base/res/res_currency_demo.xml | 1056 +++++ openerp/addons/base/res/res_currency_view.xml | 19 +- openerp/addons/base/res/res_users.py | 1 + .../addons/base/security/base_security.xml | 6 +- openerp/addons/base/tests/test_acl.py | 20 +- .../addons/base/tests/test_osv_expression.yml | 57 +- openerp/fields.py | 1 + openerp/models.py | 2 +- openerp/report/report_sxw.py | 9 +- openerp/sql_db.py | 2 +- openerp/tools/misc.py | 59 +- openerp/tools/yaml_import.py | 1 - 974 files changed, 18601 insertions(+), 189791 deletions(-) delete mode 100644 addons/account/account_cash_statement.py delete mode 100644 addons/account/account_end_fy.xml delete mode 100644 addons/account/account_financial_report.py delete mode 100644 addons/account/account_financial_report_data.xml delete mode 100644 addons/account/account_installer.xml create mode 100644 addons/account/account_journal_dashboard.py delete mode 100644 addons/account/account_menuitem.xml create mode 100644 addons/account/account_move.py delete mode 100644 addons/account/account_move_line.py create mode 100644 addons/account/account_payment.py delete mode 100644 addons/account/account_pre_install.yml delete mode 100644 addons/account/account_report.xml delete mode 100644 addons/account/account_view.xml create mode 100644 addons/account/chart_template.py delete mode 100644 addons/account/data/configurable_account_chart.xml rename addons/account/{edi => data}/invoice_action_data.xml (85%) delete mode 100644 addons/account/demo/account_bank_statement.xml create mode 100644 addons/account/demo/account_bank_statement.yml delete mode 100644 addons/account/demo/account_invoice_demo.xml create mode 100644 addons/account/demo/account_invoice_demo.yml delete mode 100644 addons/account/demo/account_minimal.xml create mode 100644 addons/account/demo/account_statement_operations.yml rename addons/{account_anglo_saxon/docs => account/doc}/account_anglo_saxon.ods (100%) rename addons/{account_anglo_saxon/docs => account/doc}/account_anglo_saxon.pdf (100%) delete mode 100644 addons/account/edi/__init__.py delete mode 100644 addons/account/edi/invoice.py delete mode 100644 addons/account/installer.py delete mode 100644 addons/account/report/account_aged_partner_balance.py delete mode 100644 addons/account/report/account_analytic_entries_report.py delete mode 100644 addons/account/report/account_analytic_entries_report_view.xml delete mode 100644 addons/account/report/account_balance.py delete mode 100644 addons/account/report/account_central_journal.py delete mode 100644 addons/account/report/account_entries_report.py delete mode 100644 addons/account/report/account_entries_report_view.xml delete mode 100644 addons/account/report/account_general_journal.py delete mode 100644 addons/account/report/account_general_ledger.py delete mode 100644 addons/account/report/account_journal.py delete mode 100644 addons/account/report/account_partner_balance.py delete mode 100644 addons/account/report/account_partner_ledger.py delete mode 100644 addons/account/report/account_print_overdue.py delete mode 100644 addons/account/report/account_report.py delete mode 100644 addons/account/report/account_report_view.xml delete mode 100644 addons/account/report/account_treasury_report.py delete mode 100644 addons/account/report/account_treasury_report_view.xml delete mode 100644 addons/account/report/common_report_header.py delete mode 100644 addons/account/report/report_vat.py delete mode 100644 addons/account/res_currency.py delete mode 100644 addons/account/static/description/account_illu_02.png delete mode 100644 addons/account/static/description/account_illu_03.png delete mode 100644 addons/account/static/description/account_sc_01.png delete mode 100644 addons/account/static/description/account_sc_05.png rename addons/{account_voucher => account}/static/description/bazile.png (100%) rename addons/{account_voucher => account}/static/description/icon.png (100%) delete mode 100644 addons/account/static/description/sc00.png create mode 100644 addons/account/static/src/css/account.css delete mode 100644 addons/account/static/src/css/account_bank_statement_reconciliation.css create mode 100644 addons/account/static/src/css/account_journal_dashboard.css delete mode 100644 addons/account/static/src/css/account_move_line_quickadd.css delete mode 100644 addons/account/static/src/css/account_move_reconciliation.css create mode 100644 addons/account/static/src/css/account_reconciliation.css rename addons/account/static/src/css/{account_bank_statement_reconciliation.scss => account_reconciliation.scss} (82%) create mode 100644 addons/account/static/src/img/graph.png create mode 100644 addons/account/static/src/js/account_journal_dashboard_widget.js create mode 100644 addons/account/static/src/js/account_payment_widget.js create mode 100644 addons/account/static/src/js/account_reconciliation_widgets.js delete mode 100644 addons/account/static/src/js/widgets.js delete mode 100644 addons/account/static/src/xml/account_bank_statement_reconciliation.xml delete mode 100644 addons/account/static/src/xml/account_move_line_quickadd.xml delete mode 100644 addons/account/static/src/xml/account_move_reconciliation.xml create mode 100644 addons/account/static/src/xml/account_payment.xml create mode 100644 addons/account/static/src/xml/account_reconciliation.xml delete mode 100644 addons/account/test/account_change_currency.yml delete mode 100644 addons/account/test/account_customer_invoice.yml delete mode 100644 addons/account/test/account_fiscalyear_close.yml create mode 100644 addons/account/test/account_minimal_test.xml delete mode 100644 addons/account/test/account_period_close.yml delete mode 100644 addons/account/test/account_sequence_test.yml delete mode 100644 addons/account/test/account_supplier_invoice.yml delete mode 100644 addons/account/test/account_test_users.yml delete mode 100644 addons/account/test/account_use_model.yml delete mode 100644 addons/account/test/account_validate_account_move.yml delete mode 100644 addons/account/test/chart_of_account.yml delete mode 100644 addons/account/test/price_accuracy00.yml delete mode 100644 addons/account/test/test_edi_invoice.yml delete mode 100644 addons/account/test/test_parent_structure.py delete mode 100644 addons/account/test/test_parent_structure.yml create mode 100644 addons/account/tests/account_test_classes.py create mode 100644 addons/account/tests/account_test_users.py create mode 100644 addons/account/tests/test_account_customer_invoice.py create mode 100644 addons/account/tests/test_account_supplier_invoice.py create mode 100644 addons/account/tests/test_account_validate_account_move.py create mode 100644 addons/account/tests/test_bank_statement_reconciliation.py create mode 100644 addons/account/tests/test_chart_of_account.py create mode 100644 addons/account/tests/test_manual_reconciliation.py create mode 100644 addons/account/tests/test_payment.py rename addons/account/{ => views}/account_analytic_view.xml (84%) rename addons/account/{ => views}/account_bank_view.xml (93%) rename addons/account/{account_assert_test.xml => views/account_end_fy.xml} (53%) rename addons/account/{ => views}/account_invoice_view.xml (58%) rename addons/account/{ => views}/account_invoice_workflow.xml (99%) create mode 100644 addons/account/views/account_journal_dashboard_view.xml create mode 100644 addons/account/views/account_menuitem.xml create mode 100644 addons/account/views/account_payment_view.xml create mode 100644 addons/account/views/account_report.xml rename addons/account/{ => views}/account_tip_data.xml (100%) rename addons/account/{ => views}/account_unit_test.xml (72%) create mode 100644 addons/account/views/account_view.xml rename addons/account/{ => views}/company_view.xml (65%) rename addons/account/{ => views}/partner_view.xml (69%) rename addons/account/{ => views}/product_view.xml (85%) delete mode 100644 addons/account/views/report_agedpartnerbalance.xml delete mode 100644 addons/account/views/report_centraljournal.xml delete mode 100644 addons/account/views/report_financial.xml delete mode 100644 addons/account/views/report_generaljournal.xml delete mode 100644 addons/account/views/report_generalledger.xml delete mode 100644 addons/account/views/report_journal.xml delete mode 100644 addons/account/views/report_overdue.xml delete mode 100644 addons/account/views/report_partnerbalance.xml delete mode 100644 addons/account/views/report_partnerledger.xml delete mode 100644 addons/account/views/report_partnerledgerother.xml delete mode 100644 addons/account/views/report_salepurchasejournal.xml delete mode 100644 addons/account/views/report_trialbalance.xml delete mode 100644 addons/account/views/report_vat.xml rename addons/account/{ => views}/res_config_view.xml (74%) delete mode 100644 addons/account/wizard/account_automatic_reconcile.py delete mode 100644 addons/account/wizard/account_automatic_reconcile_view.xml delete mode 100644 addons/account/wizard/account_change_currency.py delete mode 100644 addons/account/wizard/account_change_currency_view.xml delete mode 100644 addons/account/wizard/account_chart.py delete mode 100644 addons/account/wizard/account_chart_view.xml delete mode 100644 addons/account/wizard/account_financial_report.py delete mode 100644 addons/account/wizard/account_financial_report_view.xml delete mode 100644 addons/account/wizard/account_fiscalyear_close.py delete mode 100644 addons/account/wizard/account_fiscalyear_close_state.py delete mode 100644 addons/account/wizard/account_fiscalyear_close_state.xml delete mode 100644 addons/account/wizard/account_fiscalyear_close_view.xml delete mode 100644 addons/account/wizard/account_journal_select.py delete mode 100644 addons/account/wizard/account_journal_select_view.xml create mode 100644 addons/account/wizard/account_log_followup_note.py create mode 100644 addons/account/wizard/account_log_followup_note_view.xml delete mode 100644 addons/account/wizard/account_move_bank_reconcile.py delete mode 100644 addons/account/wizard/account_move_bank_reconcile_view.xml delete mode 100644 addons/account/wizard/account_move_line_reconcile_select.py delete mode 100644 addons/account/wizard/account_move_line_reconcile_select_view.xml delete mode 100644 addons/account/wizard/account_move_line_select.py delete mode 100644 addons/account/wizard/account_move_line_select_view.xml delete mode 100644 addons/account/wizard/account_move_line_unreconcile_select.py delete mode 100644 addons/account/wizard/account_move_line_unreconcile_select_view.xml create mode 100644 addons/account/wizard/account_move_reversal.py create mode 100644 addons/account/wizard/account_move_reversal_view.xml delete mode 100644 addons/account/wizard/account_open_closed_fiscalyear.py delete mode 100644 addons/account/wizard/account_open_closed_fiscalyear_view.xml delete mode 100644 addons/account/wizard/account_period_close.py delete mode 100644 addons/account/wizard/account_period_close_view.xml delete mode 100644 addons/account/wizard/account_reconcile_partner_process.py delete mode 100644 addons/account/wizard/account_reconcile_partner_process_view.xml delete mode 100644 addons/account/wizard/account_report_account_balance.py delete mode 100644 addons/account/wizard/account_report_account_balance_view.xml delete mode 100644 addons/account/wizard/account_report_aged_partner_balance.py delete mode 100644 addons/account/wizard/account_report_aged_partner_balance_view.xml delete mode 100644 addons/account/wizard/account_report_central_journal.py delete mode 100644 addons/account/wizard/account_report_central_journal_view.xml delete mode 100644 addons/account/wizard/account_report_common.py delete mode 100644 addons/account/wizard/account_report_common_account.py delete mode 100644 addons/account/wizard/account_report_common_journal.py delete mode 100644 addons/account/wizard/account_report_common_partner.py delete mode 100644 addons/account/wizard/account_report_common_view.xml delete mode 100644 addons/account/wizard/account_report_general_journal.py delete mode 100644 addons/account/wizard/account_report_general_journal_view.xml delete mode 100644 addons/account/wizard/account_report_general_ledger.py delete mode 100644 addons/account/wizard/account_report_general_ledger_view.xml delete mode 100644 addons/account/wizard/account_report_partner_balance.py delete mode 100644 addons/account/wizard/account_report_partner_balance_view.xml delete mode 100644 addons/account/wizard/account_report_partner_ledger.py delete mode 100644 addons/account/wizard/account_report_partner_ledger_view.xml delete mode 100644 addons/account/wizard/account_report_print_journal.py delete mode 100644 addons/account/wizard/account_report_print_journal_view.xml delete mode 100644 addons/account/wizard/account_state_open.py delete mode 100644 addons/account/wizard/account_state_open_view.xml delete mode 100644 addons/account/wizard/account_statement_from_invoice.py delete mode 100644 addons/account/wizard/account_statement_from_invoice_view.xml delete mode 100644 addons/account/wizard/account_subscription_generate.py delete mode 100644 addons/account/wizard/account_subscription_generate_view.xml delete mode 100644 addons/account/wizard/account_tax_chart.py delete mode 100644 addons/account/wizard/account_tax_chart_view.xml delete mode 100644 addons/account/wizard/account_use_model.py delete mode 100644 addons/account/wizard/account_use_model_view.xml delete mode 100644 addons/account/wizard/account_vat.py delete mode 100644 addons/account/wizard/account_vat_view.xml create mode 100644 addons/account_analytic_analysis/analytic_account_demo.yml delete mode 100644 addons/account_anglo_saxon/__init__.py delete mode 100644 addons/account_anglo_saxon/__openerp__.py delete mode 100644 addons/account_anglo_saxon/i18n/account_anglo_saxon.pot delete mode 100644 addons/account_anglo_saxon/i18n/ar.po delete mode 100644 addons/account_anglo_saxon/i18n/bg.po delete mode 100644 addons/account_anglo_saxon/i18n/bs.po delete mode 100644 addons/account_anglo_saxon/i18n/ca.po delete mode 100644 addons/account_anglo_saxon/i18n/cs.po delete mode 100644 addons/account_anglo_saxon/i18n/da.po delete mode 100644 addons/account_anglo_saxon/i18n/de.po delete mode 100644 addons/account_anglo_saxon/i18n/el.po delete mode 100644 addons/account_anglo_saxon/i18n/en_GB.po delete mode 100644 addons/account_anglo_saxon/i18n/es.po delete mode 100644 addons/account_anglo_saxon/i18n/es_AR.po delete mode 100644 addons/account_anglo_saxon/i18n/es_CR.po delete mode 100644 addons/account_anglo_saxon/i18n/es_EC.po delete mode 100644 addons/account_anglo_saxon/i18n/es_MX.po delete mode 100644 addons/account_anglo_saxon/i18n/es_PY.po delete mode 100644 addons/account_anglo_saxon/i18n/es_VE.po delete mode 100644 addons/account_anglo_saxon/i18n/et.po delete mode 100644 addons/account_anglo_saxon/i18n/fa.po delete mode 100644 addons/account_anglo_saxon/i18n/fi.po delete mode 100644 addons/account_anglo_saxon/i18n/fr.po delete mode 100644 addons/account_anglo_saxon/i18n/gl.po delete mode 100644 addons/account_anglo_saxon/i18n/gu.po delete mode 100644 addons/account_anglo_saxon/i18n/hi.po delete mode 100644 addons/account_anglo_saxon/i18n/hr.po delete mode 100644 addons/account_anglo_saxon/i18n/hu.po delete mode 100644 addons/account_anglo_saxon/i18n/id.po delete mode 100644 addons/account_anglo_saxon/i18n/it.po delete mode 100644 addons/account_anglo_saxon/i18n/ja.po delete mode 100644 addons/account_anglo_saxon/i18n/lv.po delete mode 100644 addons/account_anglo_saxon/i18n/mk.po delete mode 100644 addons/account_anglo_saxon/i18n/mn.po delete mode 100644 addons/account_anglo_saxon/i18n/nb.po delete mode 100644 addons/account_anglo_saxon/i18n/nl.po delete mode 100644 addons/account_anglo_saxon/i18n/nl_BE.po delete mode 100644 addons/account_anglo_saxon/i18n/oc.po delete mode 100644 addons/account_anglo_saxon/i18n/pl.po delete mode 100644 addons/account_anglo_saxon/i18n/pt.po delete mode 100644 addons/account_anglo_saxon/i18n/pt_BR.po delete mode 100644 addons/account_anglo_saxon/i18n/ro.po delete mode 100644 addons/account_anglo_saxon/i18n/ru.po delete mode 100644 addons/account_anglo_saxon/i18n/sk.po delete mode 100644 addons/account_anglo_saxon/i18n/sl.po delete mode 100644 addons/account_anglo_saxon/i18n/sq.po delete mode 100644 addons/account_anglo_saxon/i18n/sr@latin.po delete mode 100644 addons/account_anglo_saxon/i18n/sv.po delete mode 100644 addons/account_anglo_saxon/i18n/ta.po delete mode 100644 addons/account_anglo_saxon/i18n/tr.po delete mode 100644 addons/account_anglo_saxon/i18n/zh_CN.po delete mode 100644 addons/account_anglo_saxon/i18n/zh_TW.po delete mode 100644 addons/account_anglo_saxon/invoice.py delete mode 100644 addons/account_anglo_saxon/product.py delete mode 100644 addons/account_anglo_saxon/product_view.xml delete mode 100644 addons/account_anglo_saxon/purchase.py delete mode 100644 addons/account_anglo_saxon/stock.py create mode 100644 addons/account_asset/account_asset_demo.yml rename addons/account_asset/{account_asset_demo.xml => test/account_asset_demo_test.xml} (64%) delete mode 100644 addons/account_bank_statement_extensions/__init__.py delete mode 100644 addons/account_bank_statement_extensions/__openerp__.py delete mode 100644 addons/account_bank_statement_extensions/account_bank_statement.py delete mode 100644 addons/account_bank_statement_extensions/account_bank_statement_report.xml delete mode 100644 addons/account_bank_statement_extensions/account_bank_statement_view.xml delete mode 100644 addons/account_bank_statement_extensions/data/account_bank_statement_extensions_data.xml delete mode 100644 addons/account_bank_statement_extensions/i18n/account_bank_statement_extensions.pot delete mode 100644 addons/account_bank_statement_extensions/i18n/ar.po delete mode 100644 addons/account_bank_statement_extensions/i18n/bg.po delete mode 100644 addons/account_bank_statement_extensions/i18n/bs.po delete mode 100644 addons/account_bank_statement_extensions/i18n/ca.po delete mode 100644 addons/account_bank_statement_extensions/i18n/cs.po delete mode 100644 addons/account_bank_statement_extensions/i18n/da.po delete mode 100644 addons/account_bank_statement_extensions/i18n/de.po delete mode 100644 addons/account_bank_statement_extensions/i18n/en_GB.po delete mode 100644 addons/account_bank_statement_extensions/i18n/es.po delete mode 100644 addons/account_bank_statement_extensions/i18n/es_AR.po delete mode 100644 addons/account_bank_statement_extensions/i18n/es_CR.po delete mode 100644 addons/account_bank_statement_extensions/i18n/es_EC.po delete mode 100644 addons/account_bank_statement_extensions/i18n/es_MX.po delete mode 100644 addons/account_bank_statement_extensions/i18n/fi.po delete mode 100644 addons/account_bank_statement_extensions/i18n/fr.po delete mode 100644 addons/account_bank_statement_extensions/i18n/gu.po delete mode 100644 addons/account_bank_statement_extensions/i18n/hr.po delete mode 100644 addons/account_bank_statement_extensions/i18n/hu.po delete mode 100644 addons/account_bank_statement_extensions/i18n/it.po delete mode 100644 addons/account_bank_statement_extensions/i18n/ja.po delete mode 100644 addons/account_bank_statement_extensions/i18n/lv.po delete mode 100644 addons/account_bank_statement_extensions/i18n/mk.po delete mode 100644 addons/account_bank_statement_extensions/i18n/mn.po delete mode 100644 addons/account_bank_statement_extensions/i18n/nb.po delete mode 100644 addons/account_bank_statement_extensions/i18n/nl.po delete mode 100644 addons/account_bank_statement_extensions/i18n/pl.po delete mode 100644 addons/account_bank_statement_extensions/i18n/pt.po delete mode 100644 addons/account_bank_statement_extensions/i18n/pt_BR.po delete mode 100644 addons/account_bank_statement_extensions/i18n/ro.po delete mode 100644 addons/account_bank_statement_extensions/i18n/ru.po delete mode 100644 addons/account_bank_statement_extensions/i18n/sl.po delete mode 100644 addons/account_bank_statement_extensions/i18n/sr@latin.po delete mode 100644 addons/account_bank_statement_extensions/i18n/sv.po delete mode 100644 addons/account_bank_statement_extensions/i18n/tr.po delete mode 100644 addons/account_bank_statement_extensions/i18n/zh_CN.po delete mode 100644 addons/account_bank_statement_extensions/i18n/zh_TW.po delete mode 100644 addons/account_bank_statement_extensions/report/__init__.py delete mode 100644 addons/account_bank_statement_extensions/report/bank_statement_balance_report.py delete mode 100644 addons/account_bank_statement_extensions/res_partner_bank.py delete mode 100644 addons/account_bank_statement_extensions/security/ir.model.access.csv delete mode 100644 addons/account_bank_statement_extensions/views/report_bankstatementbalance.xml delete mode 100644 addons/account_bank_statement_extensions/wizard/__init__.py delete mode 100644 addons/account_bank_statement_extensions/wizard/cancel_statement_line.py delete mode 100644 addons/account_bank_statement_extensions/wizard/cancel_statement_line_wizard.xml delete mode 100644 addons/account_bank_statement_extensions/wizard/confirm_statement_line.py delete mode 100644 addons/account_bank_statement_extensions/wizard/confirm_statement_line_wizard.xml delete mode 100644 addons/account_bank_statement_import/demo/fiscalyear_period.xml create mode 100644 addons/account_budget/account_budget_demo.yml delete mode 100644 addons/account_check_writing/account_voucher_view.xml delete mode 100644 addons/account_followup/__init__.py delete mode 100644 addons/account_followup/__openerp__.py delete mode 100644 addons/account_followup/account_followup.py delete mode 100644 addons/account_followup/account_followup_customers.xml delete mode 100644 addons/account_followup/account_followup_data.xml delete mode 100644 addons/account_followup/account_followup_demo.xml delete mode 100644 addons/account_followup/account_followup_reports.xml delete mode 100644 addons/account_followup/account_followup_view.xml delete mode 100644 addons/account_followup/i18n/account_followup.pot delete mode 100644 addons/account_followup/i18n/ar.po delete mode 100644 addons/account_followup/i18n/bg.po delete mode 100644 addons/account_followup/i18n/bs.po delete mode 100644 addons/account_followup/i18n/ca.po delete mode 100644 addons/account_followup/i18n/cs.po delete mode 100644 addons/account_followup/i18n/da.po delete mode 100644 addons/account_followup/i18n/de.po delete mode 100644 addons/account_followup/i18n/el.po delete mode 100644 addons/account_followup/i18n/en_GB.po delete mode 100644 addons/account_followup/i18n/es.po delete mode 100644 addons/account_followup/i18n/es_AR.po delete mode 100644 addons/account_followup/i18n/es_CR.po delete mode 100644 addons/account_followup/i18n/es_EC.po delete mode 100644 addons/account_followup/i18n/es_MX.po delete mode 100644 addons/account_followup/i18n/es_PY.po delete mode 100644 addons/account_followup/i18n/es_VE.po delete mode 100644 addons/account_followup/i18n/et.po delete mode 100644 addons/account_followup/i18n/fa.po delete mode 100644 addons/account_followup/i18n/fi.po delete mode 100644 addons/account_followup/i18n/fr.po delete mode 100644 addons/account_followup/i18n/gl.po delete mode 100644 addons/account_followup/i18n/hr.po delete mode 100644 addons/account_followup/i18n/hu.po delete mode 100644 addons/account_followup/i18n/id.po delete mode 100644 addons/account_followup/i18n/it.po delete mode 100644 addons/account_followup/i18n/ja.po delete mode 100644 addons/account_followup/i18n/ko.po delete mode 100644 addons/account_followup/i18n/lt.po delete mode 100644 addons/account_followup/i18n/lv.po delete mode 100644 addons/account_followup/i18n/mk.po delete mode 100644 addons/account_followup/i18n/mn.po delete mode 100644 addons/account_followup/i18n/nb.po delete mode 100644 addons/account_followup/i18n/nl.po delete mode 100644 addons/account_followup/i18n/nl_BE.po delete mode 100644 addons/account_followup/i18n/oc.po delete mode 100644 addons/account_followup/i18n/pl.po delete mode 100644 addons/account_followup/i18n/pt.po delete mode 100644 addons/account_followup/i18n/pt_BR.po delete mode 100644 addons/account_followup/i18n/ro.po delete mode 100644 addons/account_followup/i18n/ru.po delete mode 100644 addons/account_followup/i18n/sl.po delete mode 100644 addons/account_followup/i18n/sq.po delete mode 100644 addons/account_followup/i18n/sr.po delete mode 100644 addons/account_followup/i18n/sr@latin.po delete mode 100644 addons/account_followup/i18n/sv.po delete mode 100644 addons/account_followup/i18n/tlh.po delete mode 100644 addons/account_followup/i18n/tr.po delete mode 100644 addons/account_followup/i18n/uk.po delete mode 100644 addons/account_followup/i18n/vi.po delete mode 100644 addons/account_followup/i18n/zh_CN.po delete mode 100644 addons/account_followup/i18n/zh_TW.po delete mode 100644 addons/account_followup/report/__init__.py delete mode 100644 addons/account_followup/report/account_followup_print.py delete mode 100644 addons/account_followup/report/account_followup_report.py delete mode 100644 addons/account_followup/report/account_followup_report.xml delete mode 100644 addons/account_followup/res_config_view.xml delete mode 100644 addons/account_followup/security/account_followup_security.xml delete mode 100644 addons/account_followup/security/ir.model.access.csv delete mode 100644 addons/account_followup/test/account_followup.yml delete mode 100644 addons/account_followup/tests/__init__.py delete mode 100644 addons/account_followup/tests/test_account_followup.py delete mode 100644 addons/account_followup/views/report_followup.xml delete mode 100644 addons/account_followup/wizard/__init__.py delete mode 100644 addons/account_followup/wizard/account_followup_print.py delete mode 100644 addons/account_followup/wizard/account_followup_print_view.xml delete mode 100644 addons/account_payment/__init__.py delete mode 100644 addons/account_payment/__openerp__.py delete mode 100644 addons/account_payment/account_invoice.py delete mode 100644 addons/account_payment/account_move_line.py delete mode 100644 addons/account_payment/account_payment.py delete mode 100644 addons/account_payment/account_payment_demo.xml delete mode 100644 addons/account_payment/account_payment_report.xml delete mode 100644 addons/account_payment/account_payment_sequence.xml delete mode 100644 addons/account_payment/account_payment_view.xml delete mode 100644 addons/account_payment/account_payment_workflow.xml delete mode 100644 addons/account_payment/i18n/account_payment.pot delete mode 100644 addons/account_payment/i18n/am.po delete mode 100644 addons/account_payment/i18n/ar.po delete mode 100644 addons/account_payment/i18n/bg.po delete mode 100644 addons/account_payment/i18n/bs.po delete mode 100644 addons/account_payment/i18n/ca.po delete mode 100644 addons/account_payment/i18n/cs.po delete mode 100644 addons/account_payment/i18n/da.po delete mode 100644 addons/account_payment/i18n/de.po delete mode 100644 addons/account_payment/i18n/el.po delete mode 100644 addons/account_payment/i18n/en_GB.po delete mode 100644 addons/account_payment/i18n/es.po delete mode 100644 addons/account_payment/i18n/es_AR.po delete mode 100644 addons/account_payment/i18n/es_CL.po delete mode 100644 addons/account_payment/i18n/es_CR.po delete mode 100644 addons/account_payment/i18n/es_EC.po delete mode 100644 addons/account_payment/i18n/es_MX.po delete mode 100644 addons/account_payment/i18n/es_PY.po delete mode 100644 addons/account_payment/i18n/es_VE.po delete mode 100644 addons/account_payment/i18n/et.po delete mode 100644 addons/account_payment/i18n/fa.po delete mode 100644 addons/account_payment/i18n/fi.po delete mode 100644 addons/account_payment/i18n/fr.po delete mode 100644 addons/account_payment/i18n/gl.po delete mode 100644 addons/account_payment/i18n/hi.po delete mode 100644 addons/account_payment/i18n/hr.po delete mode 100644 addons/account_payment/i18n/hu.po delete mode 100644 addons/account_payment/i18n/id.po delete mode 100644 addons/account_payment/i18n/it.po delete mode 100644 addons/account_payment/i18n/ja.po delete mode 100644 addons/account_payment/i18n/ko.po delete mode 100644 addons/account_payment/i18n/lt.po delete mode 100644 addons/account_payment/i18n/lv.po delete mode 100644 addons/account_payment/i18n/mk.po delete mode 100644 addons/account_payment/i18n/mn.po delete mode 100644 addons/account_payment/i18n/nb.po delete mode 100644 addons/account_payment/i18n/nl.po delete mode 100644 addons/account_payment/i18n/nl_BE.po delete mode 100644 addons/account_payment/i18n/oc.po delete mode 100644 addons/account_payment/i18n/pl.po delete mode 100644 addons/account_payment/i18n/pt.po delete mode 100644 addons/account_payment/i18n/pt_BR.po delete mode 100644 addons/account_payment/i18n/ro.po delete mode 100644 addons/account_payment/i18n/ru.po delete mode 100644 addons/account_payment/i18n/sl.po delete mode 100644 addons/account_payment/i18n/sq.po delete mode 100644 addons/account_payment/i18n/sr.po delete mode 100644 addons/account_payment/i18n/sr@latin.po delete mode 100644 addons/account_payment/i18n/sv.po delete mode 100644 addons/account_payment/i18n/tlh.po delete mode 100644 addons/account_payment/i18n/tr.po delete mode 100644 addons/account_payment/i18n/uk.po delete mode 100644 addons/account_payment/i18n/vi.po delete mode 100644 addons/account_payment/i18n/zh_CN.po delete mode 100644 addons/account_payment/i18n/zh_TW.po delete mode 100644 addons/account_payment/report/__init__.py delete mode 100644 addons/account_payment/report/payment_order.py delete mode 100644 addons/account_payment/security/account_payment_security.xml delete mode 100644 addons/account_payment/security/ir.model.access.csv delete mode 100644 addons/account_payment/test/account_payment_demo.yml delete mode 100644 addons/account_payment/test/account_payment_report.yml delete mode 100644 addons/account_payment/test/cancel_payment_order.yml delete mode 100644 addons/account_payment/test/payment_order_process.yml delete mode 100644 addons/account_payment/views/report_paymentorder.xml delete mode 100644 addons/account_payment/wizard/__init__.py delete mode 100644 addons/account_payment/wizard/account_payment_create_order_view.xml delete mode 100644 addons/account_payment/wizard/account_payment_order.py delete mode 100644 addons/account_payment/wizard/account_payment_pay.py delete mode 100644 addons/account_payment/wizard/account_payment_pay_view.xml delete mode 100644 addons/account_payment/wizard/account_payment_populate_statement.py delete mode 100644 addons/account_payment/wizard/account_payment_populate_statement_view.xml delete mode 100644 addons/account_sequence/__init__.py delete mode 100644 addons/account_sequence/__openerp__.py delete mode 100644 addons/account_sequence/account_sequence.py delete mode 100644 addons/account_sequence/account_sequence.xml delete mode 100644 addons/account_sequence/account_sequence_data.xml delete mode 100644 addons/account_sequence/account_sequence_installer.py delete mode 100644 addons/account_sequence/account_sequence_installer_view.xml delete mode 100644 addons/account_sequence/i18n/account_sequence.pot delete mode 100644 addons/account_sequence/i18n/ar.po delete mode 100644 addons/account_sequence/i18n/bg.po delete mode 100644 addons/account_sequence/i18n/bs.po delete mode 100644 addons/account_sequence/i18n/ca.po delete mode 100644 addons/account_sequence/i18n/cs.po delete mode 100644 addons/account_sequence/i18n/da.po delete mode 100644 addons/account_sequence/i18n/de.po delete mode 100644 addons/account_sequence/i18n/el.po delete mode 100644 addons/account_sequence/i18n/en_GB.po delete mode 100644 addons/account_sequence/i18n/es.po delete mode 100644 addons/account_sequence/i18n/es_AR.po delete mode 100644 addons/account_sequence/i18n/es_CR.po delete mode 100644 addons/account_sequence/i18n/es_EC.po delete mode 100644 addons/account_sequence/i18n/es_MX.po delete mode 100644 addons/account_sequence/i18n/es_PY.po delete mode 100644 addons/account_sequence/i18n/es_VE.po delete mode 100644 addons/account_sequence/i18n/fa.po delete mode 100644 addons/account_sequence/i18n/fr.po delete mode 100644 addons/account_sequence/i18n/gl.po delete mode 100644 addons/account_sequence/i18n/hr.po delete mode 100644 addons/account_sequence/i18n/hu.po delete mode 100644 addons/account_sequence/i18n/id.po delete mode 100644 addons/account_sequence/i18n/it.po delete mode 100644 addons/account_sequence/i18n/ja.po delete mode 100644 addons/account_sequence/i18n/lv.po delete mode 100644 addons/account_sequence/i18n/mk.po delete mode 100644 addons/account_sequence/i18n/mn.po delete mode 100644 addons/account_sequence/i18n/nb.po delete mode 100644 addons/account_sequence/i18n/nl.po delete mode 100644 addons/account_sequence/i18n/nl_BE.po delete mode 100644 addons/account_sequence/i18n/pl.po delete mode 100644 addons/account_sequence/i18n/pt.po delete mode 100644 addons/account_sequence/i18n/pt_BR.po delete mode 100644 addons/account_sequence/i18n/ro.po delete mode 100644 addons/account_sequence/i18n/ru.po delete mode 100644 addons/account_sequence/i18n/sl.po delete mode 100644 addons/account_sequence/i18n/sq.po delete mode 100644 addons/account_sequence/i18n/sr@latin.po delete mode 100644 addons/account_sequence/i18n/sv.po delete mode 100644 addons/account_sequence/i18n/tr.po delete mode 100644 addons/account_sequence/i18n/vi.po delete mode 100644 addons/account_sequence/i18n/zh_CN.po delete mode 100644 addons/account_sequence/i18n/zh_TW.po create mode 100644 addons/account_tax_python/__init__.py create mode 100644 addons/account_tax_python/__openerp__.py create mode 100644 addons/account_tax_python/account_tax_python.py create mode 100644 addons/account_tax_python/account_tax_python.xml delete mode 100644 addons/account_voucher/account_voucher_pay_invoice.xml delete mode 100644 addons/account_voucher/account_voucher_sequence.xml delete mode 100644 addons/account_voucher/account_voucher_wizard.xml delete mode 100644 addons/account_voucher/account_voucher_workflow.xml delete mode 100644 addons/account_voucher/invoice.py delete mode 100644 addons/account_voucher/static/description/account_illu_01.png delete mode 100644 addons/account_voucher/static/description/account_sc_00.png delete mode 100644 addons/account_voucher/static/description/account_sc_02.png delete mode 100644 addons/account_voucher/static/description/account_sc_03.png delete mode 100644 addons/account_voucher/static/description/account_sc_04.png delete mode 100644 addons/account_voucher/static/description/account_sc_06.png delete mode 100644 addons/account_voucher/static/description/index.html delete mode 100644 addons/account_voucher/static/description/testimonial_sunil.jpg create mode 100644 addons/account_voucher/test/account_voucher_chart.yml delete mode 100644 addons/account_voucher/test/case1_usd_usd.yml delete mode 100644 addons/account_voucher/test/case1_usd_usd_payment_rate.yml delete mode 100644 addons/account_voucher/test/case2_suppl_usd_eur.yml delete mode 100644 addons/account_voucher/test/case2_usd_eur_debtor_in_eur.yml delete mode 100644 addons/account_voucher/test/case2_usd_eur_debtor_in_usd.yml delete mode 100644 addons/account_voucher/test/case3_eur_eur.yml delete mode 100644 addons/account_voucher/test/case4_cad_chf.yml delete mode 100644 addons/account_voucher/test/case5_suppl_usd_usd.yml delete mode 100644 addons/account_voucher/test/case_eur_usd.yml delete mode 100644 addons/account_voucher/test/sales_payment.yml delete mode 100644 addons/account_voucher/voucher_payment_receipt_view.xml delete mode 100644 addons/analytic/wizard/account_analytic_journal_report.py delete mode 100644 addons/analytic/wizard/account_analytic_journal_report_view.xml delete mode 100644 addons/edi/__init__.py delete mode 100644 addons/edi/__openerp__.py delete mode 100644 addons/edi/controllers/__init__.py delete mode 100644 addons/edi/controllers/main.py delete mode 100644 addons/edi/edi_service.py delete mode 100644 addons/edi/i18n/ar.po delete mode 100644 addons/edi/i18n/bg.po delete mode 100644 addons/edi/i18n/cs.po delete mode 100644 addons/edi/i18n/de.po delete mode 100644 addons/edi/i18n/edi.pot delete mode 100644 addons/edi/i18n/en_GB.po delete mode 100644 addons/edi/i18n/es.po delete mode 100644 addons/edi/i18n/es_AR.po delete mode 100644 addons/edi/i18n/es_CR.po delete mode 100644 addons/edi/i18n/fi.po delete mode 100644 addons/edi/i18n/fr.po delete mode 100644 addons/edi/i18n/he.po delete mode 100644 addons/edi/i18n/hr.po delete mode 100644 addons/edi/i18n/hu.po delete mode 100644 addons/edi/i18n/is.po delete mode 100644 addons/edi/i18n/it.po delete mode 100644 addons/edi/i18n/ja.po delete mode 100644 addons/edi/i18n/lt.po delete mode 100644 addons/edi/i18n/mk.po delete mode 100644 addons/edi/i18n/ml.po delete mode 100644 addons/edi/i18n/mn.po delete mode 100644 addons/edi/i18n/nb.po delete mode 100644 addons/edi/i18n/nl.po delete mode 100644 addons/edi/i18n/nl_BE.po delete mode 100644 addons/edi/i18n/pl.po delete mode 100644 addons/edi/i18n/pt.po delete mode 100644 addons/edi/i18n/pt_BR.po delete mode 100644 addons/edi/i18n/ro.po delete mode 100644 addons/edi/i18n/ru.po delete mode 100644 addons/edi/i18n/sl.po delete mode 100644 addons/edi/i18n/sv.po delete mode 100644 addons/edi/i18n/ta.po delete mode 100644 addons/edi/i18n/tr.po delete mode 100644 addons/edi/i18n/zh_CN.po delete mode 100644 addons/edi/i18n/zh_TW.po delete mode 100644 addons/edi/models/__init__.py delete mode 100644 addons/edi/models/edi.py delete mode 100644 addons/edi/models/res_company.py delete mode 100644 addons/edi/models/res_currency.py delete mode 100644 addons/edi/models/res_partner.py delete mode 100644 addons/edi/static/src/img/knowledge.png delete mode 100644 addons/edi/static/src/js/edi.js delete mode 100644 addons/edi/test/edi_partner_test.yml delete mode 100644 addons/edi/views/edi.xml create mode 100644 addons/hr_timesheet/hr_timesheet_demo.yml create mode 100644 addons/hr_timesheet/test/hr_timesheet_data.xml create mode 100644 addons/mrp/mrp_demo.yml delete mode 100644 addons/multi_company/__init__.py delete mode 100644 addons/multi_company/__openerp__.py delete mode 100644 addons/multi_company/i18n/ar.po delete mode 100644 addons/multi_company/i18n/bg.po delete mode 100644 addons/multi_company/i18n/bs.po delete mode 100644 addons/multi_company/i18n/ca.po delete mode 100644 addons/multi_company/i18n/cs.po delete mode 100644 addons/multi_company/i18n/da.po delete mode 100644 addons/multi_company/i18n/de.po delete mode 100644 addons/multi_company/i18n/es.po delete mode 100644 addons/multi_company/i18n/es_AR.po delete mode 100644 addons/multi_company/i18n/es_CR.po delete mode 100644 addons/multi_company/i18n/es_EC.po delete mode 100644 addons/multi_company/i18n/es_MX.po delete mode 100644 addons/multi_company/i18n/es_VE.po delete mode 100644 addons/multi_company/i18n/et.po delete mode 100644 addons/multi_company/i18n/fi.po delete mode 100644 addons/multi_company/i18n/fr.po delete mode 100644 addons/multi_company/i18n/gl.po delete mode 100644 addons/multi_company/i18n/hr.po delete mode 100644 addons/multi_company/i18n/hu.po delete mode 100644 addons/multi_company/i18n/id.po delete mode 100644 addons/multi_company/i18n/it.po delete mode 100644 addons/multi_company/i18n/ja.po delete mode 100644 addons/multi_company/i18n/lo.po delete mode 100644 addons/multi_company/i18n/lt.po delete mode 100644 addons/multi_company/i18n/lv.po delete mode 100644 addons/multi_company/i18n/mk.po delete mode 100644 addons/multi_company/i18n/mn.po delete mode 100644 addons/multi_company/i18n/multi_company.pot delete mode 100644 addons/multi_company/i18n/nb.po delete mode 100644 addons/multi_company/i18n/nl.po delete mode 100644 addons/multi_company/i18n/oc.po delete mode 100644 addons/multi_company/i18n/pl.po delete mode 100644 addons/multi_company/i18n/pt.po delete mode 100644 addons/multi_company/i18n/pt_BR.po delete mode 100644 addons/multi_company/i18n/ro.po delete mode 100644 addons/multi_company/i18n/ru.po delete mode 100644 addons/multi_company/i18n/sl.po delete mode 100644 addons/multi_company/i18n/sr.po delete mode 100644 addons/multi_company/i18n/sr@latin.po delete mode 100644 addons/multi_company/i18n/sv.po delete mode 100644 addons/multi_company/i18n/tr.po delete mode 100644 addons/multi_company/i18n/uk.po delete mode 100644 addons/multi_company/i18n/vi.po delete mode 100644 addons/multi_company/i18n/zh_CN.po delete mode 100644 addons/multi_company/i18n/zh_TW.po delete mode 100644 addons/multi_company/multi_company_demo.xml delete mode 100644 addons/multi_company/res_company_view.xml delete mode 100644 addons/multi_company/security/ir.model.access.csv delete mode 100644 addons/point_of_sale/account_statement_demo.xml create mode 100644 addons/point_of_sale/test/tests_before.xml delete mode 100644 addons/purchase/edi/__init__.py delete mode 100644 addons/purchase/edi/purchase_order.py delete mode 100644 addons/purchase/edi/purchase_order_action_data.xml rename addons/{account_anglo_saxon => purchase}/test/anglo_saxon.yml (67%) rename addons/{account_anglo_saxon => purchase}/test/anglo_saxon_avg_fifo.yml (64%) delete mode 100644 addons/purchase/test/process/edi_purchase_order.yml rename addons/{stock_account/stock_account_demo.xml => purchase/test/stock_valuation_account.xml} (81%) delete mode 100644 addons/sale/edi/__init__.py delete mode 100644 addons/sale/edi/sale_order.py delete mode 100644 addons/sale/edi/sale_order_action_data.xml create mode 100644 addons/sale/sale_demo.yml delete mode 100644 addons/sale/test/edi_sale_order.yml create mode 100644 addons/sale_stock/sale_stock_demo.yml create mode 100644 addons/stock_account/stock_account_demo.yml create mode 100644 addons/stock_account/stock_account_post_install.yml create mode 100644 addons/stock_account/test/stock_valuation_account.xml rename addons/{website_links => web}/static/lib/zeroclipboard/ZeroClipboard.js (100%) rename addons/{website_links => web}/static/lib/zeroclipboard/ZeroClipboard.swf (100%) create mode 100644 openerp/addons/base/res/res_currency_demo.xml diff --git a/addons/account/__init__.py b/addons/account/__init__.py index 7fb712a619c..0528f0e6d40 100644 --- a/addons/account/__init__.py +++ b/addons/account/__init__.py @@ -21,18 +21,17 @@ import partner import account -import installer +import account_payment import account_invoice import account_bank_statement import account_bank -import account_cash_statement -import account_move_line +import account_move +import chart_template import account_analytic_line -import account_financial_report -import wizard -import report +import account_journal_dashboard import product import company -import res_currency -import edi import res_config + +import wizard +import report diff --git a/addons/account/__openerp__.py b/addons/account/__openerp__.py index 0cc9c538f75..1e36685fc72 100644 --- a/addons/account/__openerp__.py +++ b/addons/account/__openerp__.py @@ -19,140 +19,64 @@ # ############################################################################## { - 'name' : 'eInvoicing', + 'name' : 'Invoicing', 'version' : '1.1', 'author' : 'OpenERP SA', - 'category' : 'Accounting & Finance', - 'description' : """ -Accounting and Financial Management. -==================================== + 'summary': 'Send Invoices and Track Payments', + 'sequence': 4, + 'description': """ +Invoicing & Payments +==================== +The specific and easy-to-use Invoicing system in Odoo allows you to keep track of your accounting, even when you are not an accountant. It provides an easy way to follow up on your suppliers and customers. -Financial and accounting module that covers: --------------------------------------------- - * General Accounting - * Cost/Analytic accounting - * Third party accounting - * Taxes management - * Budgets - * Customer and Supplier Invoices - * Bank statements - * Reconciliation process by partner - -Creates a dashboard for accountants that includes: --------------------------------------------------- - * List of Customer Invoices to Approve - * Company Analysis - * Graph of Treasury - -Processes like maintaining general ledgers are done through the defined Financial Journals (entry move line or grouping is maintained through a journal) -for a particular financial year and for preparation of vouchers there is a module named account_voucher. +You could use this simplified accounting in case you work with an (external) account to keep your books, and you still want to keep track of payments. This module also offers you an easy method of registering payments, without having to encode complete abstracts of account. """, + 'category' : 'Accounting & Finance', 'website': 'https://www.odoo.com/page/billing', - 'depends': ['base_setup', 'product', 'analytic', 'board', 'edi', 'report', 'web_tip'], + 'images' : ['images/accounts.jpeg','images/bank_statement.jpeg','images/cash_register.jpeg','images/chart_of_accounts.jpeg','images/customer_invoice.jpeg','images/journal_entries.jpeg'], + 'depends' : ['base_setup', 'product', 'analytic', 'board', 'report', 'web_tip'], 'data': [ 'security/account_security.xml', 'security/ir.model.access.csv', - 'account_menuitem.xml', - 'report/account_invoice_report_view.xml', - 'report/account_entries_report_view.xml', - 'report/account_treasury_report_view.xml', - 'report/account_report_view.xml', - 'report/account_analytic_entries_report_view.xml', - 'wizard/account_move_bank_reconcile_view.xml', - 'wizard/account_use_model_view.xml', - 'account_installer.xml', - 'wizard/account_period_close_view.xml', + 'data/data_account_type.xml', + 'data/account_data.xml', + 'views/account_menuitem.xml', + 'views/account_payment_view.xml', 'wizard/account_reconcile_view.xml', 'wizard/account_unreconcile_view.xml', - 'wizard/account_statement_from_invoice_view.xml', - 'account_view.xml', - 'account_report.xml', - 'account_financial_report_data.xml', - 'wizard/account_report_common_view.xml', + 'views/account_view.xml', + 'views/account_report.xml', 'wizard/account_invoice_refund_view.xml', - 'wizard/account_fiscalyear_close_state.xml', - 'wizard/account_chart_view.xml', - 'wizard/account_tax_chart_view.xml', - 'wizard/account_move_line_reconcile_select_view.xml', - 'wizard/account_open_closed_fiscalyear_view.xml', - 'wizard/account_move_line_unreconcile_select_view.xml', - 'wizard/account_vat_view.xml', - 'wizard/account_report_print_journal_view.xml', - 'wizard/account_report_general_journal_view.xml', - 'wizard/account_report_central_journal_view.xml', - 'wizard/account_subscription_generate_view.xml', - 'wizard/account_fiscalyear_close_view.xml', - 'wizard/account_state_open_view.xml', - 'wizard/account_journal_select_view.xml', - 'wizard/account_change_currency_view.xml', + 'wizard/account_move_reversal_view.xml', 'wizard/account_validate_move_view.xml', - 'wizard/account_report_general_ledger_view.xml', 'wizard/account_invoice_state_view.xml', - 'wizard/account_report_partner_balance_view.xml', - 'wizard/account_report_account_balance_view.xml', - 'wizard/account_report_aged_partner_balance_view.xml', - 'wizard/account_report_partner_ledger_view.xml', - 'wizard/account_reconcile_partner_process_view.xml', - 'wizard/account_automatic_reconcile_view.xml', - 'wizard/account_financial_report_view.xml', + 'wizard/account_log_followup_note_view.xml', 'wizard/pos_box.xml', - 'account_end_fy.xml', - 'account_invoice_view.xml', - 'data/account_data.xml', - 'data/data_account_type.xml', - 'data/configurable_account_chart.xml', - 'account_invoice_workflow.xml', - 'partner_view.xml', - 'product_view.xml', - 'account_assert_test.xml', - 'account_analytic_view.xml', - 'company_view.xml', - 'edi/invoice_action_data.xml', - 'account_bank_view.xml', - 'res_config_view.xml', - 'account_tip_data.xml', - 'account_pre_install.yml', - 'views/report_vat.xml', - 'views/report_invoice.xml', - 'views/report_trialbalance.xml', - 'views/report_centraljournal.xml', - 'views/report_overdue.xml', - 'views/report_generaljournal.xml', - 'views/report_journal.xml', - 'views/report_salepurchasejournal.xml', - 'views/report_partnerbalance.xml', - 'views/report_agedpartnerbalance.xml', - 'views/report_partnerledger.xml', - 'views/report_partnerledgerother.xml', - 'views/report_financial.xml', - 'views/report_generalledger.xml', + 'views/account_end_fy.xml', + 'views/account_invoice_view.xml', + 'data/invoice_action_data.xml', + 'views/account_invoice_workflow.xml', + 'views/partner_view.xml', + 'views/product_view.xml', + 'views/account_analytic_view.xml', + 'views/company_view.xml', + 'views/account_bank_view.xml', + 'views/res_config_view.xml', + 'views/account_tip_data.xml', 'views/account.xml', - ], - 'qweb' : [ - "static/src/xml/account_move_reconciliation.xml", - "static/src/xml/account_move_line_quickadd.xml", - "static/src/xml/account_bank_statement_reconciliation.xml", + 'views/report_invoice.xml', + 'report/account_invoice_report_view.xml', + 'views/account_journal_dashboard_view.xml', ], 'demo': [ 'demo/account_demo.xml', - 'demo/account_minimal.xml', - 'demo/account_invoice_demo.xml', - 'demo/account_bank_statement.xml', - 'account_unit_test.xml', ], - 'test': [ - 'test/account_test_users.yml', - 'test/account_customer_invoice.yml', - 'test/account_supplier_invoice.yml', - 'test/account_change_currency.yml', - 'test/chart_of_account.yml', - 'test/account_period_close.yml', - 'test/account_use_model.yml', - 'test/account_validate_account_move.yml', - 'test/test_edi_invoice.yml', - 'test/account_report.yml', - 'test/account_fiscalyear_close.yml', #last test, as it will definitively close the demo fiscalyear + 'qweb': [ + "static/src/xml/account_reconciliation.xml", + "static/src/xml/account_payment.xml", + "static/src/xml/account_report_backend.xml", ], 'installable': True, + 'application': True, 'auto_install': False, } diff --git a/addons/account/account.py b/addons/account/account.py index d5e35d1146e..a0afbea6c08 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -1,2085 +1,452 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 datetime import datetime -from dateutil.relativedelta import relativedelta -from operator import itemgetter import time +import math -import openerp -from openerp import SUPERUSER_ID, api -from openerp import tools -from openerp.osv import fields, osv, expression -from openerp.tools.translate import _ +from openerp.osv import expression from openerp.tools.float_utils import float_round as round +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.exceptions import UserError - -import openerp.addons.decimal_precision as dp - -_logger = logging.getLogger(__name__) - -def check_cycle(self, cr, uid, ids, context=None): - """ climbs the ``self._table.parent_id`` chains for 100 levels or - until it can't find any more parent(s) - - Returns true if it runs out of parents (no cycle), false if - it can recurse 100 times without ending all chains - """ - level = 100 - while len(ids): - cr.execute('SELECT DISTINCT parent_id '\ - 'FROM '+self._table+' '\ - 'WHERE id IN %s '\ - 'AND parent_id IS NOT NULL',(tuple(ids),)) - ids = map(itemgetter(0), cr.fetchall()) - if not level: - return False - level -= 1 - return True - -class res_company(osv.osv): - _inherit = "res.company" - _columns = { - 'income_currency_exchange_account_id': fields.many2one( - 'account.account', - string="Gain Exchange Rate Account", - domain="[('type', '=', 'other')]",), - 'expense_currency_exchange_account_id': fields.many2one( - 'account.account', - string="Loss Exchange Rate Account", - domain="[('type', '=', 'other')]",), - } - -class account_payment_term(osv.osv): - _name = "account.payment.term" - _description = "Payment Term" - _columns = { - 'name': fields.char('Payment Term', translate=True, required=True), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the payment term without removing it."), - 'note': fields.text('Description', translate=True), - 'line_ids': fields.one2many('account.payment.term.line', 'payment_id', 'Terms', copy=True), - } - _defaults = { - 'active': 1, - } - _order = "name" - - def compute(self, cr, uid, id, value, date_ref=False, context=None): - if not date_ref: - date_ref = datetime.now().strftime('%Y-%m-%d') - pt = self.browse(cr, uid, id, context=context) - amount = value - result = [] - obj_precision = self.pool.get('decimal.precision') - prec = obj_precision.precision_get(cr, uid, 'Account') - for line in pt.line_ids: - if line.value == 'fixed': - amt = round(line.value_amount, prec) - elif line.value == 'procent': - amt = round(value * (line.value_amount/100.0), prec) - elif line.value == 'balance': - amt = round(amount, prec) - if amt: - next_date = (datetime.strptime(date_ref, '%Y-%m-%d') + relativedelta(days=line.days)) - if line.days2 < 0: - next_first_date = next_date + relativedelta(day=1,months=1) #Getting 1st of next month - next_date = next_first_date + relativedelta(days=line.days2) - if line.days2 > 0: - next_date += relativedelta(day=line.days2, months=1) - result.append( (next_date.strftime('%Y-%m-%d'), amt) ) - amount -= amt - - amount = reduce(lambda x,y: x+y[1], result, 0.0) - dist = round(value-amount, prec) - if dist: - result.append( (time.strftime('%Y-%m-%d'), dist) ) - return result - -class account_payment_term_line(osv.osv): - _name = "account.payment.term.line" - _description = "Payment Term Line" - _columns = { - 'value': fields.selection([('procent', 'Percent'), - ('fixed', 'Fixed Amount'), - ('balance', 'Balance')], 'Computation', - required=True, help="""Select here the kind of valuation related to this payment term line."""), - 'value_amount': fields.float('Amount To Pay', digits_compute=dp.get_precision('Payment Term')), - 'days': fields.integer('Number of Days', required=True, help="Number of days to add before computing the day of the month."), - 'days2': fields.integer('Day of the Month', required=True, help="Day of the month \n\n Set : \n1)-1 for the last day of the current month. \n2) 0 for net days\n3) A positive number for the specific day of the next month.\n\nExample : if Date=15/01, Number of Days=22, Day of Month=-1, then the due date is 28/02."), - 'payment_id': fields.many2one('account.payment.term', 'Payment Term', required=True, select=True, ondelete='cascade'), - } - _defaults = { - 'value': 'balance', - 'days': 30, - 'days2': 0, - } - _order = "value desc,days" - - def _check_percent(self, cr, uid, ids, context=None): - obj = self.browse(cr, uid, ids[0], context=context) - if obj.value == 'procent' and ( obj.value_amount < 0.0 or obj.value_amount > 100.0): - return False - return True - - _constraints = [ - (_check_percent, 'Percentages for Payment Term Line must be between 0 and 100.', ['value_amount']), - ] +from openerp import api, fields, models, _ -class account_account_type(osv.osv): +class AccountAccountType(models.Model): _name = "account.account.type" _description = "Account Type" - def _get_financial_report_ref(self, cr, uid, context=None): - obj_data = self.pool.get('ir.model.data') - obj_financial_report = self.pool.get('account.financial.report') - financial_report_ref = {} - for key, financial_report in [ - ('asset','account_financial_report_assets0'), - ('liability','account_financial_report_liability0'), - ('income','account_financial_report_income0'), - ('expense','account_financial_report_expense0'), - ]: - try: - financial_report_ref[key] = obj_financial_report.browse(cr, uid, - obj_data.get_object_reference(cr, uid, 'account', financial_report)[1], - context=context) - except ValueError: - pass - return financial_report_ref - - def _get_current_report_type(self, cr, uid, ids, name, arg, context=None): - res = {} - financial_report_ref = self._get_financial_report_ref(cr, uid, context=context) - for record in self.browse(cr, uid, ids, context=context): - res[record.id] = 'none' - for key, financial_report in financial_report_ref.items(): - list_ids = [x.id for x in financial_report.account_type_ids] - if record.id in list_ids: - res[record.id] = key - return res - - def _save_report_type(self, cr, uid, account_type_id, field_name, field_value, arg, context=None): - field_value = field_value or 'none' - obj_financial_report = self.pool.get('account.financial.report') - #unlink if it exists somewhere in the financial reports related to BS or PL - financial_report_ref = self._get_financial_report_ref(cr, uid, context=context) - for key, financial_report in financial_report_ref.items(): - list_ids = [x.id for x in financial_report.account_type_ids] - if account_type_id in list_ids: - obj_financial_report.write(cr, uid, [financial_report.id], {'account_type_ids': [(3, account_type_id)]}) - #write it in the good place - if field_value != 'none': - return obj_financial_report.write(cr, uid, [financial_report_ref[field_value].id], {'account_type_ids': [(4, account_type_id)]}) - - _columns = { - 'name': fields.char('Account Type', required=True, translate=True), - 'code': fields.char('Code', size=32, required=True, select=True), - 'close_method': fields.selection([('none', 'None'), ('balance', 'Balance'), ('detail', 'Detail'), ('unreconciled', 'Unreconciled')], 'Carry Forward Method', required=True, help="""Set here the method that will be used to generate the end of year journal entries for all the accounts of this type. - - 'None' means that nothing will be done. - 'Balance' will generally be used for cash accounts. - 'Detail' will copy each existing journal item of the previous year, even the reconciled ones. - 'Unreconciled' will copy only the journal items that were unreconciled on the first day of the new fiscal year."""), - 'report_type': fields.function(_get_current_report_type, fnct_inv=_save_report_type, type='selection', string='P&L / BS Category', store=True, - selection= [('none','/'), - ('income', _('Profit & Loss (Income account)')), - ('expense', _('Profit & Loss (Expense account)')), - ('asset', _('Balance Sheet (Asset account)')), - ('liability', _('Balance Sheet (Liability account)'))], help="This field is used to generate legal reports: profit and loss, balance sheet.", required=True), - 'note': fields.text('Description'), - } - _defaults = { - 'close_method': 'none', - 'report_type': 'none', - } - _order = "code" + name = fields.Char(string='Account Type', required=True, translate=True) + include_initial_balance = fields.Boolean(string="Bring Accounts Balance Forward", help="Used in reports to know if we should consider journal items from the beginning of time instead of from the fiscal year only. Account types that should be reset to zero at each new fiscal year (like expenses, revenue..) should not have this option set.") + type = fields.Selection([ + ('other', 'Regular'), + ('receivable', 'Receivable'), + ('payable', 'Payable'), + ('liquidity', 'Liquidity'), + ], required=True, default='other', + help="The 'Internal Type' is used for features available on "\ + "different types of accounts: liquidity type is for cash or bank accounts"\ + ", payable/receivable is for supplier/customer accounts.") + note = fields.Text(string='Description') -def _code_get(self, cr, uid, context=None): - acc_type_obj = self.pool.get('account.account.type') - ids = acc_type_obj.search(cr, uid, []) - res = acc_type_obj.read(cr, uid, ids, ['code', 'name'], context=context) - return [(r['code'], r['name']) for r in res] +class AccountAccountTag(models.Model): + _name = 'account.account.tag' + _description = 'Account Tag' + + name = fields.Char(translate=True, required=True) + applicability = fields.Selection([('accounts', 'Accounts'), ('taxes', 'Taxes')], required=True, default='accounts') #---------------------------------------------------------- # Accounts #---------------------------------------------------------- -class account_account(osv.osv): - _order = "parent_left" - _parent_order = "code" + +class AccountAccount(models.Model): _name = "account.account" _description = "Account" - _parent_store = True + _order = "code" - def search(self, cr, uid, args, offset=0, limit=None, order=None, - context=None, count=False): - if context is None: - context = {} - pos = 0 + #@api.multi + #def _compute_has_unreconciled_entries(self): + # print "ici dedans" + # account_ids = self.ids + # for account in self: + # # Avoid useless work if has_unreconciled_entries is not relevant for this account + # if account.deprecated or not account.reconcile: + # account.has_unreconciled_entries = False + # account_ids = account_ids - account + # if account_ids: + # res = dict.fromkeys([x.id for x in account_ids], False) + # self.env.cr.execute( + # """ SELECT s.account_id FROM( + # SELECT + # a.id as account_id, a.last_time_entries_checked AS last_time_entries_checked, + # MAX(l.write_date) AS max_date + # FROM + # account_move_line l + # RIGHT JOIN account_account a ON (a.id = l.account_id) + # WHERE + # a.id in %s + # AND EXISTS ( + # SELECT 1 + # FROM account_move_line l + # WHERE l.account_id = a.id + # AND l.amount_residual > 0 + # ) + # AND EXISTS ( + # SELECT 1 + # FROM account_move_line l + # WHERE l.account_id = a.id + # AND l.amount_residual < 0 + # ) + # GROUP BY a.id, a.last_time_entries_checked + # ) as s + # WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) + # """ % (account_ids,)) + # res.update(self.env.cr.dictfetchall()) + # for account in self.browse(res.keys()): + # if res[account.id]: + # account.has_unreconciled_entries = True - while pos < len(args): + name = fields.Char(required=True, index=True) + currency_id = fields.Many2one('res.currency', string='Secondary Currency', + help="Forces all moves for this account to have this secondary currency.") + code = fields.Char(size=64, required=True, index=True) + deprecated = fields.Boolean(index=True, default=False) + user_type = fields.Many2one('account.account.type', string='Type', required=True, + help="Account Type is used for information purpose, to generate country-specific legal reports, and set the rules to close a fiscal year and generate opening entries.") + internal_type = fields.Selection(related='user_type.type', store=True) + #has_unreconciled_entries = fields.Boolean(compute='_compute_has_unreconciled_entries', + # help="The account has at least one unreconciled debit and credit since last time the invoices & payments matching was performed.") + last_time_entries_checked = fields.Datetime(string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, + help='Last time the invoices & payments matching was performed on this account. It is set either if there\'s not at least '\ + 'an unreconciled debit and an unreconciled credit Or if you click the "Done" button.') + reconcile = fields.Boolean(string='Allow Invoices & Payments Matching', default=False, + help="Check this box if this account allows invoices & payments matching of journal items.") + tax_ids = fields.Many2many('account.tax', 'account_account_tax_default_rel', + 'account_id', 'tax_id', string='Default Taxes') + note = fields.Text('Internal Notes') + company_id = fields.Many2one('res.company', string='Company', required=True, + default=lambda self: self.env['res.company']._company_default_get('account.account')) + tag_ids = fields.Many2many('account.account.tag', 'account_account_account_tag', string='Account tag', help="Optional tags you may want to assign for custom reporting") - if args[pos][0] == 'code' and args[pos][1] in ('like', 'ilike') and args[pos][2]: - args[pos] = ('code', '=like', tools.ustr(args[pos][2].replace('%', ''))+'%') - if args[pos][0] == 'journal_id': - if not args[pos][2]: - del args[pos] - continue - jour = self.pool.get('account.journal').browse(cr, uid, args[pos][2], context=context) - if (not (jour.account_control_ids or jour.type_control_ids)) or not args[pos][2]: - args[pos] = ('type','not in',('consolidation','view')) - continue - ids3 = map(lambda x: x.id, jour.type_control_ids) - ids1 = super(account_account, self).search(cr, uid, [('user_type', 'in', ids3)]) - ids1 += map(lambda x: x.id, jour.account_control_ids) - args[pos] = ('id', 'in', ids1) - pos += 1 - - if context and context.has_key('consolidate_children'): #add consolidated children of accounts - ids = super(account_account, self).search(cr, uid, args, offset, limit, - order, context=context, count=count) - for consolidate_child in self.browse(cr, uid, context['account_id'], context=context).child_consol_ids: - ids.append(consolidate_child.id) - return ids - - return super(account_account, self).search(cr, uid, args, offset, limit, - order, context=context, count=count) - - def _get_children_and_consol(self, cr, uid, ids, context=None): - #this function search for all the children and all consolidated children (recursively) of the given account ids - ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)], context=context) - ids3 = [] - for rec in self.browse(cr, uid, ids2, context=context): - for child in rec.child_consol_ids: - ids3.append(child.id) - if ids3: - ids3 = self._get_children_and_consol(cr, uid, ids3, context) - return ids2 + ids3 - - def __compute(self, cr, uid, ids, field_names, arg=None, context=None, - query='', query_params=()): - """ compute the balance, debit and/or credit for the provided - account ids - Arguments: - `ids`: account ids - `field_names`: the fields to compute (a list of any of - 'balance', 'debit' and 'credit') - `arg`: unused fields.function stuff - `query`: additional query filter (as a string) - `query_params`: parameters for the provided query string - (__compute will handle their escaping) as a - tuple - """ - mapping = { - 'balance': "COALESCE(SUM(l.debit),0) - COALESCE(SUM(l.credit), 0) as balance", - 'debit': "COALESCE(SUM(l.debit), 0) as debit", - 'credit': "COALESCE(SUM(l.credit), 0) as credit", - # by convention, foreign_balance is 0 when the account has no secondary currency, because the amounts may be in different currencies - 'foreign_balance': "(SELECT CASE WHEN currency_id IS NULL THEN 0 ELSE COALESCE(SUM(l.amount_currency), 0) END FROM account_account WHERE id IN (l.account_id)) as foreign_balance", - } - #get all the necessary accounts - children_and_consolidated = self._get_children_and_consol(cr, uid, ids, context=context) - #compute for each account the balance/debit/credit from the move lines - accounts = {} - res = {} - null_result = dict((fn, 0.0) for fn in field_names) - if children_and_consolidated: - aml_query = self.pool.get('account.move.line')._query_get(cr, uid, context=context) - - wheres = [""] - if query.strip(): - wheres.append(query.strip()) - if aml_query.strip(): - wheres.append(aml_query.strip()) - filters = " AND ".join(wheres) - # IN might not work ideally in case there are too many - # children_and_consolidated, in that case join on a - # values() e.g.: - # SELECT l.account_id as id FROM account_move_line l - # INNER JOIN (VALUES (id1), (id2), (id3), ...) AS tmp (id) - # ON l.account_id = tmp.id - # or make _get_children_and_consol return a query and join on that - request = ("SELECT l.account_id as id, " +\ - ', '.join(mapping.values()) + - " FROM account_move_line l" \ - " WHERE l.account_id IN %s " \ - + filters + - " GROUP BY l.account_id") - params = (tuple(children_and_consolidated),) + query_params - cr.execute(request, params) - - for row in cr.dictfetchall(): - accounts[row['id']] = row - - # consolidate accounts with direct children - children_and_consolidated.reverse() - brs = list(self.browse(cr, uid, children_and_consolidated, context=context)) - sums = {} - currency_obj = self.pool.get('res.currency') - while brs: - current = brs.pop(0) -# can_compute = True -# for child in current.child_id: -# if child.id not in sums: -# can_compute = False -# try: -# brs.insert(0, brs.pop(brs.index(child))) -# except ValueError: -# brs.insert(0, child) -# if can_compute: - for fn in field_names: - sums.setdefault(current.id, {})[fn] = accounts.get(current.id, {}).get(fn, 0.0) - for child in current.child_id: - if child.company_id.currency_id.id == current.company_id.currency_id.id: - sums[current.id][fn] += sums[child.id][fn] - else: - sums[current.id][fn] += currency_obj.compute(cr, uid, child.company_id.currency_id.id, current.company_id.currency_id.id, sums[child.id][fn], context=context) - - # as we have to relay on values computed before this is calculated separately than previous fields - if current.currency_id and current.exchange_rate and \ - ('adjusted_balance' in field_names or 'unrealized_gain_loss' in field_names): - # Computing Adjusted Balance and Unrealized Gains and losses - # Adjusted Balance = Foreign Balance / Exchange Rate - # Unrealized Gains and losses = Adjusted Balance - Balance - adj_bal = sums[current.id].get('foreign_balance', 0.0) / current.exchange_rate - sums[current.id].update({'adjusted_balance': adj_bal, 'unrealized_gain_loss': adj_bal - sums[current.id].get('balance', 0.0)}) - - for id in ids: - res[id] = sums.get(id, null_result) - else: - for id in ids: - res[id] = null_result - return res - - def _get_company_currency(self, cr, uid, ids, field_name, arg, context=None): - result = {} - for rec in self.browse(cr, uid, ids, context=context): - result[rec.id] = (rec.company_id.currency_id.id,rec.company_id.currency_id.symbol) - return result - - def _get_child_ids(self, cr, uid, ids, field_name, arg, context=None): - result = {} - for record in self.browse(cr, uid, ids, context=context): - if record.child_parent_ids: - result[record.id] = [x.id for x in record.child_parent_ids] - else: - result[record.id] = [] - - if record.child_consol_ids: - for acc in record.child_consol_ids: - if acc.id not in result[record.id]: - result[record.id].append(acc.id) - - return result - - def _get_level(self, cr, uid, ids, field_name, arg, context=None): - res = {} - for account in self.browse(cr, uid, ids, context=context): - #we may not know the level of the parent at the time of computation, so we - # can't simply do res[account.id] = account.parent_id.level + 1 - level = 0 - parent = account.parent_id - while parent: - level += 1 - parent = parent.parent_id - res[account.id] = level - return res - - def _set_credit_debit(self, cr, uid, account_id, name, value, arg, context=None): - if context.get('config_invisible', True): - return True - - account = self.browse(cr, uid, account_id, context=context) - diff = value - getattr(account,name) - if not diff: - return True - - journal_obj = self.pool.get('account.journal') - jids = journal_obj.search(cr, uid, [('type','=','situation'),('centralisation','=',1),('company_id','=',account.company_id.id)], context=context) - if not jids: - raise UserError(_("You need an Opening journal with centralisation checked to set the initial balance.")) - - period_obj = self.pool.get('account.period') - pids = period_obj.search(cr, uid, [('special','=',True),('company_id','=',account.company_id.id)], context=context) - if not pids: - raise UserError(_("There is no opening/closing period defined, please create one to set the initial balance.")) - - move_obj = self.pool.get('account.move.line') - move_id = move_obj.search(cr, uid, [ - ('journal_id','=',jids[0]), - ('period_id','=',pids[0]), - ('account_id','=', account_id), - (name,'>', 0.0), - ('name','=', _('Opening Balance')) - ], context=context) - if move_id: - move = move_obj.browse(cr, uid, move_id[0], context=context) - move_obj.write(cr, uid, move_id[0], { - name: diff+getattr(move,name) - }, context=context) - else: - if diff<0.0: - raise UserError(_("Unable to adapt the initial balance (negative value).")) - nameinv = (name=='credit' and 'debit') or 'credit' - move_id = move_obj.create(cr, uid, { - 'name': _('Opening Balance'), - 'account_id': account_id, - 'journal_id': jids[0], - 'period_id': pids[0], - name: diff, - nameinv: 0.0 - }, context=context) - return True - - _columns = { - 'name': fields.char('Name', required=True, select=True), - 'currency_id': fields.many2one('res.currency', 'Secondary Currency', help="Forces all moves for this account to have this secondary currency."), - 'code': fields.char('Code', size=64, required=True, select=1), - 'type': fields.selection([ - ('view', 'View'), - ('other', 'Regular'), - ('receivable', 'Receivable'), - ('payable', 'Payable'), - ('liquidity','Liquidity'), - ('consolidation', 'Consolidation'), - ('closed', 'Closed'), - ], 'Internal Type', required=True, help="The 'Internal Type' is used for features available on "\ - "different types of accounts: view can not have journal items, consolidation are accounts that "\ - "can have children accounts for multi-company consolidations, payable/receivable are for "\ - "partners accounts (for debit/credit computations), closed for depreciated accounts."), - 'user_type': fields.many2one('account.account.type', 'Account Type', required=True, - help="Account Type is used for information purpose, to generate " - "country-specific legal reports, and set the rules to close a fiscal year and generate opening entries."), - 'financial_report_ids': fields.many2many('account.financial.report', 'account_account_financial_report', 'account_id', 'report_line_id', 'Financial Reports'), - 'parent_id': fields.many2one('account.account', 'Parent', ondelete='cascade', domain=[('type','=','view')]), - 'child_parent_ids': fields.one2many('account.account','parent_id','Children'), - 'child_consol_ids': fields.many2many('account.account', 'account_account_consol_rel', 'child_id', 'parent_id', 'Consolidated Children'), - 'child_id': fields.function(_get_child_ids, type='many2many', relation="account.account", string="Child Accounts"), - 'balance': fields.function(__compute, digits_compute=dp.get_precision('Account'), string='Balance', multi='balance'), - 'credit': fields.function(__compute, fnct_inv=_set_credit_debit, digits_compute=dp.get_precision('Account'), string='Credit', multi='balance'), - 'debit': fields.function(__compute, fnct_inv=_set_credit_debit, digits_compute=dp.get_precision('Account'), string='Debit', multi='balance'), - 'foreign_balance': fields.function(__compute, digits_compute=dp.get_precision('Account'), string='Foreign Balance', multi='balance', - help="Total amount (in Secondary currency) for transactions held in secondary currency for this account."), - 'adjusted_balance': fields.function(__compute, digits_compute=dp.get_precision('Account'), string='Adjusted Balance', multi='balance', - help="Total amount (in Company currency) for transactions held in secondary currency for this account."), - 'unrealized_gain_loss': fields.function(__compute, digits_compute=dp.get_precision('Account'), string='Unrealized Gain or Loss', multi='balance', - help="Value of Loss or Gain due to changes in exchange rate when doing multi-currency transactions."), - 'reconcile': fields.boolean('Allow Reconciliation', help="Check this box if this account allows reconciliation of journal items."), - 'exchange_rate': fields.related('currency_id', 'rate', type='float', string='Exchange Rate', digits=(12,6)), - 'shortcut': fields.char('Shortcut', size=12), - 'tax_ids': fields.many2many('account.tax', 'account_account_tax_default_rel', - 'account_id', 'tax_id', 'Default Taxes'), - 'note': fields.text('Internal Notes'), - 'company_currency_id': fields.function(_get_company_currency, type='many2one', relation='res.currency', string='Company Currency'), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'active': fields.boolean('Active', select=2, help="If the active field is set to False, it will allow you to hide the account without removing it."), - - 'parent_left': fields.integer('Parent Left', select=1), - 'parent_right': fields.integer('Parent Right', select=1), - 'currency_mode': fields.selection([('current', 'At Date'), ('average', 'Average Rate')], 'Outgoing Currencies Rate', - help= - 'This will select how the current currency rate for outgoing transactions is computed. '\ - 'In most countries the legal method is "average" but only a few software systems are able to '\ - 'manage this. So if you import from another software system you may have to use the rate at date. ' \ - 'Incoming transactions always use the rate at date.', \ - required=True), - 'level': fields.function(_get_level, string='Level', method=True, type='integer', - store={ - 'account.account': (_get_children_and_consol, ['level', 'parent_id'], 10), - }), - } - - _defaults = { - 'type': 'other', - 'reconcile': False, - 'active': True, - 'currency_mode': 'current', - 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'account.account', context=c), - } - - def _check_recursion(self, cr, uid, ids, context=None): - obj_self = self.browse(cr, uid, ids[0], context=context) - p_id = obj_self.parent_id and obj_self.parent_id.id - if (obj_self in obj_self.child_consol_ids) or (p_id and (p_id is obj_self.id)): - return False - while(ids): - cr.execute('SELECT DISTINCT child_id '\ - 'FROM account_account_consol_rel '\ - 'WHERE parent_id IN %s', (tuple(ids),)) - child_ids = map(itemgetter(0), cr.fetchall()) - c_ids = child_ids - if (p_id and (p_id in c_ids)) or (obj_self.id in c_ids): - return False - while len(c_ids): - s_ids = self.search(cr, uid, [('parent_id', 'in', c_ids)]) - if p_id and (p_id in s_ids): - return False - c_ids = s_ids - ids = child_ids - return True - - def _check_type(self, cr, uid, ids, context=None): - if context is None: - context = {} - accounts = self.browse(cr, uid, ids, context=context) - for account in accounts: - if account.child_id and account.type not in ('view', 'consolidation'): - return False - return True - - def _check_account_type(self, cr, uid, ids, context=None): - for account in self.browse(cr, uid, ids, context=context): - if account.type in ('receivable', 'payable') and account.user_type.close_method != 'unreconciled': - return False - return True - - def _check_company_account(self, cr, uid, ids, context=None): - for account in self.browse(cr, uid, ids, context=context): - if account.parent_id: - if account.company_id != account.parent_id.company_id: - return False - return True - - _constraints = [ - (_check_recursion, 'Error!\nYou cannot create recursive accounts.', ['parent_id']), - (_check_type, 'Configuration Error!\nYou cannot define children to an account with internal type different of "View".', ['type']), - (_check_account_type, 'Configuration Error!\nYou cannot select an account type with a `carry forward` method different of "Unreconciled" for accounts with internal type "Payable/Receivable".', ['user_type','type']), - (_check_company_account, 'Error!\nYou cannot create an account which has parent account of different company.', ['parent_id']), - ] _sql_constraints = [ ('code_company_uniq', 'unique (code,company_id)', 'The code of the account must be unique per company !') ] - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100): - if not args: - args = [] - args = args[:] - ids = [] - try: - if name and str(name).startswith('partner:'): - part_id = int(name.split(':')[1]) - part = self.pool.get('res.partner').browse(cr, user, part_id, context=context) - args += [('id', 'in', (part.property_account_payable.id, part.property_account_receivable.id))] - name = False - if name and str(name).startswith('type:'): - type = name.split(':')[1] - args += [('type', '=', type)] - name = False - except: - pass + + @api.model + def name_search(self, name, args=None, operator='ilike', limit=100): + args = args or [] + domain = [] if name: - if operator not in expression.NEGATIVE_TERM_OPERATORS: - plus_percent = lambda n: n+'%' - code_op, code_conv = { - 'ilike': ('=ilike', plus_percent), - 'like': ('=like', plus_percent), - }.get(operator, (operator, lambda n: n)) + domain = ['|', ('code', '=ilike', name + '%'), ('name', operator, name)] + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = ['&'] + domain + accounts = self.search(domain + args, limit=limit) + return accounts.name_get() - ids = self.search(cr, user, ['|', ('code', code_op, code_conv(name)), '|', ('shortcut', '=', name), ('name', operator, name)]+args, limit=limit) + @api.multi + @api.depends('name', 'code') + def name_get(self): + result = [] + for account in self: + name = account.code + ' ' + account.name + result.append((account.id, name)) + return result - if not ids and len(name.split()) >= 2: - #Separating code and name of account for searching - operand1,operand2 = name.split(' ',1) #name can contain spaces e.g. OpenERP S.A. - ids = self.search(cr, user, [('code', operator, operand1), ('name', operator, operand2)]+ args, limit=limit) - else: - ids = self.search(cr, user, ['&','!', ('code', '=like', name+"%"), ('name', operator, name)]+args, limit=limit) - # as negation want to restric, do if already have results - if ids and len(name.split()) >= 2: - operand1,operand2 = name.split(' ',1) #name can contain spaces e.g. OpenERP S.A. - ids = self.search(cr, user, [('code', operator, operand1), ('name', operator, operand2), ('id', 'in', ids)]+ args, limit=limit) - else: - ids = self.search(cr, user, args, context=context, limit=limit) - return self.name_get(cr, user, ids, context=context) + @api.one + def copy(self, default=None): + default = dict(default or {}) + default.update(code=_("%s (copy)") % (self.code or '')) + return super(AccountAccount, self).copy(default) - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - if isinstance(ids, (int, long)): - ids = [ids] - reads = self.read(cr, uid, ids, ['name', 'code'], context=context) - res = [] - for record in reads: - name = record['name'] - if record['code']: - name = record['code'] + ' ' + name - res.append((record['id'], name)) - return res + @api.multi + def write(self, vals): + # Dont allow changing the company_id when account_move_line already exist + if vals.get('company_id', False): + move_lines = self.env['account.move.line'].search([('account_id', 'in', self.ids)], limit=1) + for account in self: + if (account.company_id.id <> vals['company_id']) and move_lines: + raise UserError(_('You cannot change the owner company of an account that already contains journal items.')) + return super(AccountAccount, self).write(vals) - def copy(self, cr, uid, id, default=None, context=None, done_list=None, local=False): - default = {} if default is None else default.copy() - if done_list is None: - done_list = [] - account = self.browse(cr, uid, id, context=context) - new_child_ids = [] - default.update(code=_("%s (copy)") % (account['code'] or '')) - if not local: - done_list = [] - if account.id in done_list: - return False - done_list.append(account.id) - if account: - for child in account.child_id: - child_ids = self.copy(cr, uid, child.id, default, context=context, done_list=done_list, local=True) - if child_ids: - new_child_ids.append(child_ids) - default['child_parent_ids'] = [(6, 0, new_child_ids)] - else: - default['child_parent_ids'] = False - return super(account_account, self).copy(cr, uid, id, default, context=context) - - def _check_moves(self, cr, uid, ids, method, context=None): - line_obj = self.pool.get('account.move.line') - account_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context) - - if line_obj.search(cr, uid, [('account_id', 'in', account_ids)], context=context): - if method == 'write': - raise UserError(_('You cannot deactivate an account that contains journal items.')) - elif method == 'unlink': - raise UserError(_('You cannot remove an account that contains journal items.')) + @api.multi + def unlink(self): + if self.env['account.move.line'].search([('account_id', 'in', self.ids)], limit=1): + raise UserError(_('You cannot do that on an account that contains journal items.')) #Checking whether the account is set as a property to any Partner or not - values = ['account.account,%s' % (account_id,) for account_id in ids] - partner_prop_acc = self.pool.get('ir.property').search(cr, uid, [('value_reference','in', values)], context=context) + values = ['account.account,%s' % (account_id,) for account_id in self.ids] + partner_prop_acc = self.env['ir.property'].search([('value_reference', 'in', values)], limit=1) if partner_prop_acc: raise UserError(_('You cannot remove/deactivate an account which is set on a customer or supplier.')) - return True + return super(AccountAccount, self).unlink() - def _check_allow_type_change(self, cr, uid, ids, new_type, context=None): - restricted_groups = ['consolidation','view'] - line_obj = self.pool.get('account.move.line') - for account in self.browse(cr, uid, ids, context=context): - old_type = account.type - account_ids = self.search(cr, uid, [('id', 'child_of', [account.id])]) - if line_obj.search(cr, uid, [('account_id', 'in', account_ids)]): - #Check for 'Closed' type - if old_type == 'closed' and new_type !='closed': - raise UserError(_("You cannot change the type of account from 'Closed' to any other type as it contains journal items!")) - # Forbid to change an account type for restricted_groups as it contains journal items (or if one of its children does) - if (new_type in restricted_groups): - raise UserError(_("You cannot change the type of account to '%s' type as it contains journal items!") % (new_type,)) + @api.multi + def mark_as_reconciled(self): + return self.write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) - return True - # For legal reason (forbiden to modify journal entries which belongs to a closed fy or period), Forbid to modify - # the code of an account if journal entries have been already posted on this account. This cannot be simply - # 'configurable' since it can lead to a lack of confidence in Odoo and this is what we want to change. - def _check_allow_code_change(self, cr, uid, ids, context=None): - line_obj = self.pool.get('account.move.line') - for account in self.browse(cr, uid, ids, context=context): - account_ids = self.search(cr, uid, [('id', 'child_of', [account.id])], context=context) - if line_obj.search(cr, uid, [('account_id', 'in', account_ids)], context=context): - raise UserError(_("You cannot change the code of account which contains journal items!")) - return True - - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - if not ids: - return True - if isinstance(ids, (int, long)): - ids = [ids] - - # Dont allow changing the company_id when account_move_line already exist - if 'company_id' in vals: - move_lines = self.pool.get('account.move.line').search(cr, uid, [('account_id', 'in', ids)], context=context) - if move_lines: - # Allow the write if the value is the same - for i in [i['company_id'][0] for i in self.read(cr,uid,ids,['company_id'], context=context)]: - if vals['company_id']!=i: - raise UserError(_('You cannot change the owner company of an account that already contains journal items.')) - if 'active' in vals and not vals['active']: - self._check_moves(cr, uid, ids, "write", context=context) - if 'type' in vals.keys(): - self._check_allow_type_change(cr, uid, ids, vals['type'], context=context) - if 'code' in vals.keys(): - self._check_allow_code_change(cr, uid, ids, context=context) - return super(account_account, self).write(cr, uid, ids, vals, context=context) - - def unlink(self, cr, uid, ids, context=None): - self._check_moves(cr, uid, ids, "unlink", context=context) - return super(account_account, self).unlink(cr, uid, ids, context=context) - -class account_journal(osv.osv): +class AccountJournal(models.Model): _name = "account.journal" _description = "Journal" - _columns = { - 'with_last_closing_balance': fields.boolean('Opening With Last Closing Balance', help="For cash or bank journal, this option should be unchecked when the starting balance should always set to 0 for new documents."), - 'name': fields.char('Journal Name', required=True), - 'code': fields.char('Code', size=5, required=True, help="The code will be displayed on reports."), - 'type': fields.selection([('sale', 'Sale'),('sale_refund','Sale Refund'), ('purchase', 'Purchase'), ('purchase_refund','Purchase Refund'), ('cash', 'Cash'), ('bank', 'Bank and Checks'), ('general', 'General'), ('situation', 'Opening/Closing Situation')], 'Type', size=32, required=True, - help="Select 'Sale' for customer invoices journals."\ - " Select 'Purchase' for supplier invoices journals."\ - " Select 'Cash' or 'Bank' for journals that are used in customer or supplier payments."\ - " Select 'General' for miscellaneous operations journals."\ - " Select 'Opening/Closing Situation' for entries generated for new fiscal years."), - 'type_control_ids': fields.many2many('account.account.type', 'account_journal_type_rel', 'journal_id','type_id', 'Type Controls', domain=[('code','<>','view'), ('code', '<>', 'closed')]), - 'account_control_ids': fields.many2many('account.account', 'account_account_type_rel', 'journal_id','account_id', 'Account', domain=[('type','<>','view'), ('type', '<>', 'closed')]), - 'default_credit_account_id': fields.many2one('account.account', 'Default Credit Account', domain="[('type','!=','view')]", help="It acts as a default account for credit amount"), - 'default_debit_account_id': fields.many2one('account.account', 'Default Debit Account', domain="[('type','!=','view')]", help="It acts as a default account for debit amount"), - 'centralisation': fields.boolean('Centralized Counterpart', help="Check this box to determine that each entry of this journal won't create a new counterpart but will share the same counterpart. This is used in fiscal year closing."), - 'update_posted': fields.boolean('Allow Cancelling Entries', help="Check this box if you want to allow the cancellation the entries related to this journal or of the invoice related to this journal"), - 'group_invoice_lines': fields.boolean('Group Invoice Lines', help="If this box is checked, the system will try to group the accounting lines when generating them from invoices."), - 'sequence_id': fields.many2one('ir.sequence', 'Entry Sequence', help="This field contains the information related to the numbering of the journal entries of this journal.", required=True, copy=False), - 'user_id': fields.many2one('res.users', 'User', help="The user responsible for this journal"), - 'groups_id': fields.many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', 'Groups'), - 'currency': fields.many2one('res.currency', 'Currency', help='The currency used to enter statement'), - 'entry_posted': fields.boolean('Autopost Created Moves', help='Check this box to automatically post entries of this journal. Note that legally, some entries may be automatically posted when the source document is validated (Invoices), whatever the status of this field.'), - 'company_id': fields.many2one('res.company', 'Company', required=True, select=1, help="Company related to this journal"), - 'allow_date':fields.boolean('Check Date in Period', help= 'If checked, the entry won\'t be created if the entry date is not included into the selected period'), - 'profit_account_id' : fields.many2one('account.account', 'Profit Account'), - 'loss_account_id' : fields.many2one('account.account', 'Loss Account'), - 'internal_account_id' : fields.many2one('account.account', 'Internal Transfers Account', select=1), - 'cash_control' : fields.boolean('Cash Control', help='If you want the journal should be control at opening/closing, check this option'), - 'analytic_journal_id':fields.many2one('account.analytic.journal','Analytic Journal', help="Journal for analytic entries"), - 'sequence': fields.integer('Sequence',help='Used to order Journals'), - } + _order = 'sequence, type, code' + + def _default_inbound_payment_methods(self): + return [(4, self.env.ref('account.account_payment_method_manual_in').id)] + + def _default_outbound_payment_methods(self): + return [(4, self.env.ref('account.account_payment_method_manual_out').id)] + + name = fields.Char(string='Journal Name', required=True) + code = fields.Char(string='Short Name', size=5, required=True, help="The code will be displayed on reports.") + type = fields.Selection([ + ('sale', 'Sale'), + ('purchase', 'Purchase'), + ('cash', 'Cash'), + ('bank', 'Bank'), + ('general', 'Miscellaneous'), + ], required=True, + help="Select 'Sale' for customer invoices journals."\ + " Select 'Purchase' for supplier bills journals."\ + " Select 'Cash' or 'Bank' for journals that are used in customer or supplier payments."\ + " Select 'General' for miscellaneous operations journals."\ + " Select 'Opening/Closing Situation' for entries generated for new fiscal years.") + type_control_ids = fields.Many2many('account.account.type', 'account_journal_type_rel', 'journal_id', 'type_id', string='Account Types Allowed (empty for no control)') + account_control_ids = fields.Many2many('account.account', 'account_account_type_rel', 'journal_id', 'account_id', string='Accounts Allowed (empty for no control)', + domain=[('deprecated', '=', False)]) + default_credit_account_id = fields.Many2one('account.account', string='Default Credit Account', + domain=[('deprecated', '=', False)], help="It acts as a default account for credit amount") + default_debit_account_id = fields.Many2one('account.account', string='Default Debit Account', + domain=[('deprecated', '=', False)], help="It acts as a default account for debit amount") + update_posted = fields.Boolean(string='Allow Cancelling Entries', + help="Check this box if you want to allow the cancellation the entries related to this journal or of the invoice related to this journal") + group_invoice_lines = fields.Boolean(string='Group Invoice Lines', + help="If this box is checked, the system will try to group the accounting lines when generating them from invoices.") + sequence_id = fields.Many2one('ir.sequence', string='Entry Sequence', + help="This field contains the information related to the numbering of the journal entries of this journal.", required=True, copy=False) + refund_sequence_id = fields.Many2one('ir.sequence', string='Refund Entry Sequence', + help="This field contains the information related to the numbering of the refund entries of this journal.", copy=False) + sequence = fields.Integer(help='Used to order Journals in the dashboard view') + + #groups_id = fields.Many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', string='Groups') + currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', oldname='currency') + company_id = fields.Many2one('res.company', string='Company', required=True, index=1, default=lambda self: self.env.user.company_id, + help="Company related to this journal") + + analytic_journal_id = fields.Many2one('account.analytic.journal', string='Analytic Journal', help="Journal for analytic entries") + refund_sequence = fields.Boolean(string='Dedicated Refund Sequence', help="Check this box if you don't want to share the same sequence for invoices and refunds made from this journal") + + inbound_payment_methods = fields.Many2many('account.payment.method', 'account_journal_inbound_payment_method_rel', 'journal_id', 'inbound_payment_method', + domain=[('payment_type', '=', 'inbound')], string='Inbound Payment Methods', default=lambda self: self._default_inbound_payment_methods(), + help="Means of payment for collecting money. Odoo modules offer various payments handling facilities, " + "but you can always use the 'Manual' payment method in order to manage payments outside of the software.") + outbound_payment_methods = fields.Many2many('account.payment.method', 'account_journal_outbound_payment_method_rel', 'journal_id', 'outbound_payment_method', + domain=[('payment_type', '=', 'outbound')], string='Outbound Payment Methods', default=lambda self: self._default_outbound_payment_methods(), + help="Means of payment for sending money. Odoo modules offer various payments handling facilities, " + "but you can always use the 'Manual' payment method in order to manage payments outside of the software.") + at_least_one_inbound = fields.Boolean(compute='_methods_compute', store=True) + at_least_one_outbound = fields.Boolean(compute='_methods_compute', store=True) + profit_account_id = fields.Many2one('account.account', string='Profit Account', domain=[('deprecated', '=', False)], help="Used to register a profit when the ending balance of a cash register differs from what the system computes") + loss_account_id = fields.Many2one('account.account', string='Loss Account', domain=[('deprecated', '=', False)], help="Used to register a loss when the ending balance of a cash register differs from what the system computes") - _defaults = { - 'cash_control' : False, - 'with_last_closing_balance' : True, - 'user_id': lambda self, cr, uid, context: uid, - 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - 'sequence': 1, - } _sql_constraints = [ - ('code_company_uniq', 'unique (code, company_id)', 'The code of the journal must be unique per company !'), - ('name_company_uniq', 'unique (name, company_id)', 'The name of the journal must be unique per company !'), + ('code_company_uniq', 'unique (code, name, company_id)', 'The code and name of the journal must be unique per company !'), ] - _order = 'sequence,code' + @api.one + @api.constrains('currency_id', 'default_credit_account_id', 'default_debit_account_id') + def _check_currency(self): + if self.currency_id: + if self.default_credit_account_id and not self.default_credit_account_id.currency_id.id == self.currency_id.id: + raise UserError(_('Configuration error!\nThe currency of the journal should be the same than the default credit account.')) + if self.default_debit_account_id and not self.default_debit_account_id.currency_id.id == self.currency_id.id: + raise UserError(_('Configuration error!\nThe currency of the journal should be the same than the default debit account.')) - def _check_currency(self, cr, uid, ids, context=None): - for journal in self.browse(cr, uid, ids, context=context): - if journal.currency: - if journal.default_credit_account_id and not journal.default_credit_account_id.currency_id.id == journal.currency.id: - return False - if journal.default_debit_account_id and not journal.default_debit_account_id.currency_id.id == journal.currency.id: - return False - return True + @api.onchange('default_debit_account_id') + def onchange_debit_account_id(self): + if not self.default_credit_account_id: + self.default_credit_account_id = self.default_debit_account_id - _constraints = [ - (_check_currency, 'Configuration error!\nThe currency chosen should be shared by the default accounts too.', ['currency','default_debit_account_id','default_credit_account_id']), - ] + @api.onchange('default_credit_account_id') + def onchange_credit_account_id(self): + if not self.default_debit_account_id: + self.default_debit_account_id = self.default_credit_account_id - def copy(self, cr, uid, id, default=None, context=None): - default = dict(context or {}) - journal = self.browse(cr, uid, id, context=context) + @api.one + def copy(self, default=None): + default = dict(default or {}) default.update( - code=_("%s (copy)") % (journal['code'] or ''), - name=_("%s (copy)") % (journal['name'] or '')) - return super(account_journal, self).copy(cr, uid, id, default, context=context) + code=_("%s (copy)") % (self.code or ''), + name=_("%s (copy)") % (self.name or '')) + return super(AccountJournal, self).copy(default) - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - if isinstance(ids, (int, long)): - ids = [ids] - for journal in self.browse(cr, uid, ids, context=context): + @api.multi + def write(self, vals): + for journal in self: if 'company_id' in vals and journal.company_id.id != vals['company_id']: - move_lines = self.pool.get('account.move.line').search(cr, uid, [('journal_id', 'in', ids)]) - if move_lines: + if self.env['account.move.line'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its company field.')) - return super(account_journal, self).write(cr, uid, ids, vals, context=context) + return super(AccountJournal, self).write(vals) - def create_sequence(self, cr, uid, vals, context=None): + @api.model + def _create_sequence(self, vals, refund=False): """ Create new no_gap entry sequence for every new Joural """ - # in account.journal code is actually the prefix of the sequence - # whereas ir.sequence code is a key to lookup global sequences. prefix = vals['code'].upper() - + if refund: + prefix = 'R' + prefix seq = { 'name': vals['name'], - 'implementation':'no_gap', - 'prefix': prefix + "/%(year)s/", + 'implementation': 'no_gap', + 'prefix': prefix + '/%(year)s/', 'padding': 4, 'number_increment': 1, 'use_date_range': True, } + if 'company_id' in vals: seq['company_id'] = vals['company_id'] - return self.pool.get('ir.sequence').create(cr, uid, seq) + return self.env['ir.sequence'].create(seq) - def create(self, cr, uid, vals, context=None): - if not 'sequence_id' in vals or not vals['sequence_id']: - # if we have the right to create a journal, we should be able to - # create it's sequence. - vals.update({'sequence_id': self.create_sequence(cr, SUPERUSER_ID, vals, context)}) - return super(account_journal, self).create(cr, uid, vals, context) + @api.model + def create(self, vals): + # We just need to create the relevant sequences according to the chosen options + if not vals.get('sequence_id'): + vals.update({'sequence_id': self.sudo()._create_sequence(vals).id}) + if vals.get('refund_sequence') and not vals.get('refund_sequence_id'): + vals.update({'refund_sequence_id': self.sudo()._create_sequence(vals, refund=True).id}) + return super(AccountJournal, self).create(vals) - def name_get(self, cr, user, ids, context=None): - """ - Returns a list of tupples containing id, name. - result format: {[(id, name), (id, name), ...]} - - @param cr: A database cursor - @param user: ID of the user currently logged in - @param ids: list of ids for which name should be read - @param context: context arguments, like lang, time zone - - @return: Returns a list of tupples containing id, name - """ - if not ids: - return [] - if isinstance(ids, (int, long)): - ids = [ids] - result = self.browse(cr, user, ids, context=context) + @api.multi + @api.depends('name', 'currency_id', 'company_id', 'company_id.currency_id') + def name_get(self): res = [] - for rs in result: - if rs.currency: - currency = rs.currency - else: - currency = rs.company_id.currency_id - name = "%s (%s)" % (rs.name, currency.name) - res += [(rs.id, name)] + for journal in self: + currency = journal.currency_id or journal.company_id.currency_id + name = "%s (%s)" % (journal.name, currency.name) + res += [(journal.id, name)] return res - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100): - if not args: - args = [] - if operator in expression.NEGATIVE_TERM_OPERATORS: - domain = [('code', operator, name), ('name', operator, name)] - else: - domain = ['|', ('code', operator, name), ('name', operator, name)] - ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) - return self.name_get(cr, user, ids, context=context) - - -class account_fiscalyear(osv.osv): - _name = "account.fiscalyear" - _description = "Fiscal Year" - _columns = { - 'name': fields.char('Fiscal Year', required=True), - 'code': fields.char('Code', size=6, required=True), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'date_start': fields.date('Start Date', required=True), - 'date_stop': fields.date('End Date', required=True), - 'period_ids': fields.one2many('account.period', 'fiscalyear_id', 'Periods'), - 'state': fields.selection([('draft','Open'), ('done','Closed')], 'Status', readonly=True, copy=False), - 'end_journal_period_id': fields.many2one( - 'account.journal.period', 'End of Year Entries Journal', - readonly=True, copy=False), - } - _defaults = { - 'state': 'draft', - 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - } - _order = "date_start, id" - - - def _check_duration(self, cr, uid, ids, context=None): - obj_fy = self.browse(cr, uid, ids[0], context=context) - if obj_fy.date_stop < obj_fy.date_start: - return False - return True - - _constraints = [ - (_check_duration, 'Error!\nThe start date of a fiscal year must precede its end date.', ['date_start','date_stop']) - ] - - def create_period3(self, cr, uid, ids, context=None): - return self.create_period(cr, uid, ids, context, 3) - - def create_period(self, cr, uid, ids, context=None, interval=1): - period_obj = self.pool.get('account.period') - for fy in self.browse(cr, uid, ids, context=context): - ds = datetime.strptime(fy.date_start, '%Y-%m-%d') - period_obj.create(cr, uid, { - 'name': "%s %s" % (_('Opening Period'), ds.strftime('%Y')), - 'code': ds.strftime('00/%Y'), - 'date_start': ds, - 'date_stop': ds, - 'special': True, - 'fiscalyear_id': fy.id, - }) - while ds.strftime('%Y-%m-%d') < fy.date_stop: - de = ds + relativedelta(months=interval, days=-1) - - if de.strftime('%Y-%m-%d') > fy.date_stop: - de = datetime.strptime(fy.date_stop, '%Y-%m-%d') - - period_obj.create(cr, uid, { - 'name': ds.strftime('%m/%Y'), - 'code': ds.strftime('%m/%Y'), - 'date_start': ds.strftime('%Y-%m-%d'), - 'date_stop': de.strftime('%Y-%m-%d'), - 'fiscalyear_id': fy.id, - }) - ds = ds + relativedelta(months=interval) - return True - - def find(self, cr, uid, dt=None, exception=True, context=None): - res = self.finds(cr, uid, dt, exception, context=context) - return res and res[0] or False - - def finds(self, cr, uid, dt=None, exception=True, context=None): - if context is None: context = {} - if not dt: - dt = fields.date.context_today(self,cr,uid,context=context) - args = [('date_start', '<=' ,dt), ('date_stop', '>=', dt)] - if context.get('company_id', False): - company_id = context['company_id'] - else: - company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - args.append(('company_id', '=', company_id)) - ids = self.search(cr, uid, args, context=context) - if not ids: - if exception: - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_fiscalyear') - msg = _('No accounting period is covering this date: %s.') % dt - raise openerp.exceptions.RedirectWarning(msg, action_id, _(' Configure Fiscal Year Now')) - else: - return [] - return ids - - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): - if args is None: - args = [] - if operator in expression.NEGATIVE_TERM_OPERATORS: - domain = [('code', operator, name), ('name', operator, name)] - else: - domain = ['|', ('code', operator, name), ('name', operator, name)] - ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) - return self.name_get(cr, user, ids, context=context) - - -class account_period(osv.osv): - _name = "account.period" - _description = "Account period" - _columns = { - 'name': fields.char('Period Name', required=True), - 'code': fields.char('Code', size=12), - 'special': fields.boolean('Opening/Closing Period',help="These periods can overlap."), - 'date_start': fields.date('Start of Period', required=True, states={'done':[('readonly',True)]}), - 'date_stop': fields.date('End of Period', required=True, states={'done':[('readonly',True)]}), - 'fiscalyear_id': fields.many2one('account.fiscalyear', 'Fiscal Year', required=True, states={'done':[('readonly',True)]}, select=True), - 'state': fields.selection([('draft','Open'), ('done','Closed')], 'Status', readonly=True, copy=False, - help='When monthly periods are created. The status is \'Draft\'. At the end of monthly period it is in \'Done\' status.'), - 'company_id': fields.related('fiscalyear_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True) - } - _defaults = { - 'state': 'draft', - } - _order = "date_start, special desc" - _sql_constraints = [ - ('name_company_uniq', 'unique(name, company_id)', 'The name of the period must be unique per company!'), - ] - - def _check_duration(self,cr,uid,ids,context=None): - obj_period = self.browse(cr, uid, ids[0], context=context) - if obj_period.date_stop < obj_period.date_start: - return False - return True - - def _check_year_limit(self,cr,uid,ids,context=None): - for obj_period in self.browse(cr, uid, ids, context=context): - if obj_period.special: - continue - - if obj_period.fiscalyear_id.date_stop < obj_period.date_stop or \ - obj_period.fiscalyear_id.date_stop < obj_period.date_start or \ - obj_period.fiscalyear_id.date_start > obj_period.date_start or \ - obj_period.fiscalyear_id.date_start > obj_period.date_stop: - return False - - pids = self.search(cr, uid, [('date_stop','>=',obj_period.date_start),('date_start','<=',obj_period.date_stop),('special','=',False),('id','<>',obj_period.id)]) - for period in self.browse(cr, uid, pids): - if period.fiscalyear_id.company_id.id==obj_period.fiscalyear_id.company_id.id: - return False - return True - - _constraints = [ - (_check_duration, 'Error!\nThe duration of the Period(s) is/are invalid.', ['date_stop']), - (_check_year_limit, 'Error!\nThe period is invalid. Either some periods are overlapping or the period\'s dates are not matching the scope of the fiscal year.', ['date_stop']) - ] - - @api.returns('self') - def next(self, cr, uid, period, step, context=None): - ids = self.search(cr, uid, [('date_start','>',period.date_start)]) - if len(ids)>=step: - return ids[step-1] - return False - - @api.returns('self') - def find(self, cr, uid, dt=None, context=None): - if context is None: context = {} - if not dt: - dt = fields.date.context_today(self, cr, uid, context=context) - args = [('date_start', '<=' ,dt), ('date_stop', '>=', dt)] - if context.get('company_id', False): - args.append(('company_id', '=', context['company_id'])) - else: - company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - args.append(('company_id', '=', company_id)) - result = [] - if context.get('account_period_prefer_normal', True): - # look for non-special periods first, and fallback to all if no result is found - result = self.search(cr, uid, args + [('special', '=', False)], context=context) - if not result: - result = self.search(cr, uid, args, context=context) - if not result: - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_period') - msg = _('No accounting period is covering this date: %s.') % dt - raise openerp.exceptions.RedirectWarning(msg, action_id, _('Configure Periods Now')) - return result - - def action_draft(self, cr, uid, ids, context=None): - mode = 'draft' - for period in self.browse(cr, uid, ids): - if period.fiscalyear_id.state == 'done': - raise UserError(_('You can not re-open a period which belongs to closed fiscal year')) - cr.execute('update account_journal_period set state=%s where period_id in %s', (mode, tuple(ids),)) - cr.execute('update account_period set state=%s where id in %s', (mode, tuple(ids),)) - self.invalidate_cache(cr, uid, context=context) - return True - - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100): - if args is None: - args = [] - if operator in expression.NEGATIVE_TERM_OPERATORS: - domain = [('code', operator, name), ('name', operator, name)] - else: - domain = ['|', ('code', operator, name), ('name', operator, name)] - ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) - return self.name_get(cr, user, ids, context=context) - - def write(self, cr, uid, ids, vals, context=None): - if 'company_id' in vals: - move_lines = self.pool.get('account.move.line').search(cr, uid, [('period_id', 'in', ids)]) - if move_lines: - raise UserError(_('This journal already contains items for this period, therefore you cannot modify its company field.')) - return super(account_period, self).write(cr, uid, ids, vals, context=context) - - def build_ctx_periods(self, cr, uid, period_from_id, period_to_id): - if period_from_id == period_to_id: - return [period_from_id] - period_from = self.browse(cr, uid, period_from_id) - period_date_start = period_from.date_start - company1_id = period_from.company_id.id - period_to = self.browse(cr, uid, period_to_id) - period_date_stop = period_to.date_stop - company2_id = period_to.company_id.id - if company1_id != company2_id: - raise UserError(_('You should choose the periods that belong to the same company.')) - if period_date_start > period_date_stop: - raise UserError(_('Start period should precede then end period.')) - - # /!\ We do not include a criterion on the company_id field below, to allow producing consolidated reports - # on multiple companies. It will only work when start/end periods are selected and no fiscal year is chosen. - - #for period from = january, we want to exclude the opening period (but it has same date_from, so we have to check if period_from is special or not to include that clause or not in the search). - if period_from.special: - return self.search(cr, uid, [('date_start', '>=', period_date_start), ('date_stop', '<=', period_date_stop)]) - return self.search(cr, uid, [('date_start', '>=', period_date_start), ('date_stop', '<=', period_date_stop), ('special', '=', False)]) - - -class account_journal_period(osv.osv): - _name = "account.journal.period" - _description = "Journal Period" - - def _icon_get(self, cr, uid, ids, field_name, arg=None, context=None): - result = {}.fromkeys(ids, 'STOCK_NEW') - for r in self.read(cr, uid, ids, ['state']): - result[r['id']] = { - 'draft': 'STOCK_NEW', - 'printed': 'STOCK_PRINT_PREVIEW', - 'done': 'STOCK_DIALOG_AUTHENTICATION', - }.get(r['state'], 'STOCK_NEW') - return result - - _columns = { - 'name': fields.char('Journal-Period Name', required=True), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True, ondelete="cascade"), - 'period_id': fields.many2one('account.period', 'Period', required=True, ondelete="cascade"), - 'icon': fields.function(_icon_get, string='Icon', type='char'), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the journal period without removing it."), - 'state': fields.selection([('draft','Draft'), ('printed','Printed'), ('done','Done')], 'Status', required=True, readonly=True, - help='When journal period is created. The status is \'Draft\'. If a report is printed it comes to \'Printed\' status. When all transactions are done, it comes in \'Done\' status.'), - 'fiscalyear_id': fields.related('period_id', 'fiscalyear_id', string='Fiscal Year', type='many2one', relation='account.fiscalyear'), - 'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True) - } - - def _check(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - cr.execute('select * from account_move_line where journal_id=%s and period_id=%s limit 1', (obj.journal_id.id, obj.period_id.id)) - res = cr.fetchall() - if res: - raise UserError(_('You cannot modify/delete a journal with entries for this period.')) - return True - - def write(self, cr, uid, ids, vals, context=None): - self._check(cr, uid, ids, context=context) - return super(account_journal_period, self).write(cr, uid, ids, vals, context=context) - - def create(self, cr, uid, vals, context=None): - period_id = vals.get('period_id',False) - if period_id: - period = self.pool.get('account.period').browse(cr, uid, period_id, context=context) - vals['state']=period.state - return super(account_journal_period, self).create(cr, uid, vals, context) - - def unlink(self, cr, uid, ids, context=None): - self._check(cr, uid, ids, context=context) - return super(account_journal_period, self).unlink(cr, uid, ids, context=context) - - _defaults = { - 'state': 'draft', - 'active': True, - } - _order = "period_id" - -#---------------------------------------------------------- -# Entries -#---------------------------------------------------------- -class account_move(osv.osv): - _name = "account.move" - _description = "Account Entry" - _order = 'id desc' - - def account_assert_balanced(self, cr, uid, context=None): - cr.execute("""\ - SELECT move_id - FROM account_move_line - WHERE state = 'valid' - GROUP BY move_id - HAVING abs(sum(debit) - sum(credit)) > 0.00001 - """) - assert len(cr.fetchall()) == 0, \ - "For all Journal Items, the state is valid implies that the sum " \ - "of credits equals the sum of debits" - return True - - def account_move_prepare(self, cr, uid, journal_id, date=False, ref='', company_id=False, context=None): - ''' - Prepares and returns a dictionary of values, ready to be passed to create() based on the parameters received. - ''' - if not date: - date = fields.date.today() - period_obj = self.pool.get('account.period') - if not company_id: - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - company_id = user.company_id.id - if context is None: - context = {} - #put the company in context to find the good period - ctx = context.copy() - ctx.update({'company_id': company_id}) - return { - 'journal_id': journal_id, - 'date': date, - 'period_id': period_obj.find(cr, uid, date, context=ctx)[0], - 'ref': ref, - 'company_id': company_id, - } - - def name_get(self, cursor, user, ids, context=None): - if isinstance(ids, (int, long)): - ids = [ids] - if not ids: - return [] - res = [] - data_move = self.pool.get('account.move').browse(cursor, user, ids, context=context) - for move in data_move: - if move.state=='draft': - name = '*' + str(move.id) - else: - name = move.name - res.append((move.id, name)) - return res - - def _get_period(self, cr, uid, context=None): - ctx = dict(context or {}) - period_ids = self.pool.get('account.period').find(cr, uid, context=ctx) - return period_ids[0] - - def _amount_compute(self, cr, uid, ids, name, args, context, where =''): - if not ids: return {} - cr.execute( 'SELECT move_id, SUM(debit) '\ - 'FROM account_move_line '\ - 'WHERE move_id IN %s '\ - 'GROUP BY move_id', (tuple(ids),)) - result = dict(cr.fetchall()) - for id in ids: - result.setdefault(id, 0.0) - return result - - def _search_amount(self, cr, uid, obj, name, args, context): - ids = set() - for cond in args: - amount = cond[2] - if isinstance(cond[2],(list,tuple)): - if cond[1] in ['in','not in']: - amount = tuple(cond[2]) - else: - continue - else: - if cond[1] in ['=like', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of']: - continue - - cr.execute("select move_id from account_move_line group by move_id having sum(debit) %s %%s" % (cond[1]),(amount,)) - res_ids = set(id[0] for id in cr.fetchall()) - ids = ids and (ids & res_ids) or res_ids - if ids: - return [('id', 'in', tuple(ids))] - return [('id', '=', '0')] - - def _get_move_from_lines(self, cr, uid, ids, context=None): - line_obj = self.pool.get('account.move.line') - return [line.move_id.id for line in line_obj.browse(cr, uid, ids, context=context)] - - _columns = { - 'name': fields.char('Number', required=True, copy=False), - 'ref': fields.char('Reference', copy=False), - 'period_id': fields.many2one('account.period', 'Period', required=True, states={'posted':[('readonly',True)]}), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True, states={'posted':[('readonly',True)]}), - 'state': fields.selection( - [('draft','Unposted'), ('posted','Posted')], 'Status', - required=True, readonly=True, copy=False, - help='All manually created new journal entries are usually in the status \'Unposted\', ' - 'but you can set the option to skip that status on the related journal. ' - 'In that case, they will behave as journal entries automatically created by the ' - 'system on document validation (invoices, bank statements...) and will be created ' - 'in \'Posted\' status.'), - 'line_id': fields.one2many('account.move.line', 'move_id', 'Entries', - states={'posted':[('readonly',True)]}, - copy=True), - 'to_check': fields.boolean('To Review', help='Check this box if you are unsure of that journal entry and if you want to note it as \'to be reviewed\' by an accounting expert.'), - 'partner_id': fields.related('line_id', 'partner_id', type="many2one", relation="res.partner", string="Partner", store={ - _name: (lambda self, cr,uid,ids,c: ids, ['line_id'], 10), - 'account.move.line': (_get_move_from_lines, ['partner_id'],10) - }), - 'amount': fields.function(_amount_compute, string='Amount', digits_compute=dp.get_precision('Account'), type='float', fnct_search=_search_amount), - 'date': fields.date('Date', required=True, states={'posted':[('readonly',True)]}, select=True), - 'narration':fields.text('Internal Note'), - 'company_id': fields.related('journal_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True), - 'balance': fields.float('balance', digits_compute=dp.get_precision('Account'), help="This is a field only used for internal purpose and shouldn't be displayed"), - 'statement_line_id': fields.many2one('account.bank.statement.line', 'Bank statement line reconciled with this entry', copy=False, readonly=True) - } - - _defaults = { - 'name': '/', - 'state': 'draft', - 'period_id': _get_period, - 'date': fields.date.context_today, - 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - } - - def _check_centralisation(self, cursor, user, ids, context=None): - for move in self.browse(cursor, user, ids, context=context): - if move.journal_id.centralisation: - move_ids = self.search(cursor, user, [ - ('period_id', '=', move.period_id.id), - ('journal_id', '=', move.journal_id.id), - ]) - if len(move_ids) > 1: - return False - return True - - _constraints = [ - (_check_centralisation, - 'You cannot create more than one move per period on a centralized journal.', - ['journal_id']), - ] - - def post(self, cr, uid, ids, context=None): - if context is None: - context = {} - invoice = context.get('invoice', False) - valid_moves = self.validate(cr, uid, ids, context) - - if not valid_moves: - raise UserError(_('You cannot validate a non-balanced entry.\nMake sure you have configured payment terms properly.\nThe latest payment term line should be of the "Balance" type.')) - obj_sequence = self.pool.get('ir.sequence') - for move in self.browse(cr, uid, valid_moves, context=context): - if move.name =='/': - new_name = False - journal = move.journal_id - - if invoice and invoice.internal_number: - new_name = invoice.internal_number - else: - if journal.sequence_id: - c = {'ir_sequence_date': move.period_id.date_start} - new_name = obj_sequence.next_by_id(cr, uid, journal.sequence_id.id, c) - else: - raise UserError(_('Please define a sequence on the journal.')) - - if new_name: - self.write(cr, uid, [move.id], {'name':new_name}) - - cr.execute('UPDATE account_move '\ - 'SET state=%s '\ - 'WHERE id IN %s', - ('posted', tuple(valid_moves),)) - self.invalidate_cache(cr, uid, context=context) - return True - - def button_validate(self, cursor, user, ids, context=None): - for move in self.browse(cursor, user, ids, context=context): - # check that all accounts have the same topmost ancestor - top_common = None - for line in move.line_id: - account = line.account_id - top_account = account - while top_account.parent_id: - top_account = top_account.parent_id - if not top_common: - top_common = top_account - elif top_account.id != top_common.id: - raise UserError(_('You cannot validate this journal entry because account "%s" does not belong to chart of accounts "%s".') % (account.name, top_common.name)) - return self.post(cursor, user, ids, context=context) - - def button_cancel(self, cr, uid, ids, context=None): - for line in self.browse(cr, uid, ids, context=context): - if not line.journal_id.update_posted: - raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) - if ids: - cr.execute('UPDATE account_move '\ - 'SET state=%s '\ - 'WHERE id IN %s', ('draft', tuple(ids),)) - self.invalidate_cache(cr, uid, context=context) - return True - - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - c = context.copy() - c['novalidate'] = True - result = super(account_move, self).write(cr, uid, ids, vals, c) - self.validate(cr, uid, ids, context=context) - return result - - def create(self, cr, uid, vals, context=None): - context = dict(context or {}) - if vals.get('line_id'): - if vals.get('journal_id'): - for l in vals['line_id']: - if not l[0]: - l[2]['journal_id'] = vals['journal_id'] - context['journal_id'] = vals['journal_id'] - if 'period_id' in vals: - for l in vals['line_id']: - if not l[0]: - l[2]['period_id'] = vals['period_id'] - context['period_id'] = vals['period_id'] - else: - default_period = self._get_period(cr, uid, context) - for l in vals['line_id']: - if not l[0]: - l[2]['period_id'] = default_period - context['period_id'] = default_period - - c = context.copy() - c['novalidate'] = True - c['period_id'] = vals['period_id'] if 'period_id' in vals else self._get_period(cr, uid, context) - c['journal_id'] = vals['journal_id'] - if 'date' in vals: c['date'] = vals['date'] - result = super(account_move, self).create(cr, uid, vals, c) - tmp = self.validate(cr, uid, [result], context) - journal = self.pool.get('account.journal').browse(cr, uid, vals['journal_id'], context) - if journal.entry_posted and tmp: - self.button_validate(cr,uid, [result], context) - else: - result = super(account_move, self).create(cr, uid, vals, context) - return result - - def unlink(self, cr, uid, ids, context=None, check=True): - context = dict(context or {}) - if isinstance(ids, (int, long)): - ids = [ids] - toremove = [] - obj_move_line = self.pool.get('account.move.line') - for move in self.browse(cr, uid, ids, context=context): - if move['state'] != 'draft': - raise UserError(_('You cannot delete a posted journal entry "%s".') % move['name']) - for line in move.line_id: - if line.invoice: - raise UserError(_("Move cannot be deleted if linked to an invoice. (Invoice: %s - Move ID:%s)") % (line.invoice.number,move.name)) - line_ids = map(lambda x: x.id, move.line_id) - context['journal_id'] = move.journal_id.id - context['period_id'] = move.period_id.id - obj_move_line._update_check(cr, uid, line_ids, context) - obj_move_line.unlink(cr, uid, line_ids, context=context) - toremove.append(move.id) - result = super(account_move, self).unlink(cr, uid, toremove, context) - return result - - def _compute_balance(self, cr, uid, id, context=None): - move = self.browse(cr, uid, id, context=context) - amount = 0 - for line in move.line_id: - amount+= (line.debit - line.credit) - return amount - - def _centralise(self, cr, uid, move, mode, context=None): - assert mode in ('debit', 'credit'), 'Invalid Mode' #to prevent sql injection - currency_obj = self.pool.get('res.currency') - account_move_line_obj = self.pool.get('account.move.line') - context = dict(context or {}) - - if mode=='credit': - account_id = move.journal_id.default_debit_account_id.id - mode2 = 'debit' - if not account_id: - raise UserError(_('There is no default debit account defined \non journal "%s".') % move.journal_id.name) - else: - account_id = move.journal_id.default_credit_account_id.id - mode2 = 'credit' - if not account_id: - raise UserError(_('There is no default credit account defined \non journal "%s".') % move.journal_id.name) - - # find the first line of this move with the current mode - # or create it if it doesn't exist - cr.execute('select id from account_move_line where move_id=%s and centralisation=%s limit 1', (move.id, mode)) - res = cr.fetchone() - if res: - line_id = res[0] - else: - context.update({'journal_id': move.journal_id.id, 'period_id': move.period_id.id}) - line_id = account_move_line_obj.create(cr, uid, { - 'name': _(mode.capitalize()+' Centralisation'), - 'centralisation': mode, - 'partner_id': False, - 'account_id': account_id, - 'move_id': move.id, - 'journal_id': move.journal_id.id, - 'period_id': move.period_id.id, - 'date': move.period_id.date_stop, - 'debit': 0.0, - 'credit': 0.0, - }, context) - - # find the first line of this move with the other mode - # so that we can exclude it from our calculation - cr.execute('select id from account_move_line where move_id=%s and centralisation=%s limit 1', (move.id, mode2)) - res = cr.fetchone() - if res: - line_id2 = res[0] - else: - line_id2 = 0 - - cr.execute('SELECT SUM(%s) FROM account_move_line WHERE move_id=%%s AND id!=%%s' % (mode,), (move.id, line_id2)) - result = cr.fetchone()[0] or 0.0 - cr.execute('update account_move_line set '+mode2+'=%s where id=%s', (result, line_id)) - account_move_line_obj.invalidate_cache(cr, uid, [mode2], [line_id], context=context) - - #adjust also the amount in currency if needed - cr.execute("select currency_id, sum(amount_currency) as amount_currency from account_move_line where move_id = %s and currency_id is not null group by currency_id", (move.id,)) - for row in cr.dictfetchall(): - currency_id = currency_obj.browse(cr, uid, row['currency_id'], context=context) - if not currency_obj.is_zero(cr, uid, currency_id, row['amount_currency']): - amount_currency = row['amount_currency'] * -1 - account_id = amount_currency > 0 and move.journal_id.default_debit_account_id.id or move.journal_id.default_credit_account_id.id - cr.execute('select id from account_move_line where move_id=%s and centralisation=\'currency\' and currency_id = %slimit 1', (move.id, row['currency_id'])) - res = cr.fetchone() - if res: - cr.execute('update account_move_line set amount_currency=%s , account_id=%s where id=%s', (amount_currency, account_id, res[0])) - account_move_line_obj.invalidate_cache(cr, uid, ['amount_currency', 'account_id'], [res[0]], context=context) - else: - context.update({'journal_id': move.journal_id.id, 'period_id': move.period_id.id}) - line_id = account_move_line_obj.create(cr, uid, { - 'name': _('Currency Adjustment'), - 'centralisation': 'currency', - 'partner_id': False, - 'account_id': account_id, - 'move_id': move.id, - 'journal_id': move.journal_id.id, - 'period_id': move.period_id.id, - 'date': move.period_id.date_stop, - 'debit': 0.0, - 'credit': 0.0, - 'currency_id': row['currency_id'], - 'amount_currency': amount_currency, - }, context) - - return True - - # - # Validate a balanced move. If it is a centralised journal, create a move. - # - def validate(self, cr, uid, ids, context=None): - if context and ('__last_update' in context): - del context['__last_update'] - - valid_moves = [] #Maintains a list of moves which can be responsible to create analytic entries - obj_analytic_line = self.pool.get('account.analytic.line') - obj_move_line = self.pool.get('account.move.line') - for move in self.browse(cr, uid, ids, context): - journal = move.journal_id - amount = 0 - line_ids = [] - line_draft_ids = [] - company_id = None - # makes sure we don't use outdated period - obj_move_line._update_journal_check(cr, uid, journal.id, move.period_id.id, context=context) - for line in move.line_id: - amount += line.debit - line.credit - line_ids.append(line.id) - if line.state=='draft': - line_draft_ids.append(line.id) - - if not company_id: - company_id = line.account_id.company_id.id - if not company_id == line.account_id.company_id.id: - raise UserError(_("Cannot create moves for different companies.")) - - if line.account_id.currency_id and line.currency_id: - if line.account_id.currency_id.id != line.currency_id.id and (line.account_id.currency_id.id != line.account_id.company_id.currency_id.id): - raise UserError(_("""Cannot create move with currency different from ..""") % (line.account_id.code, line.account_id.name)) - - if abs(amount) < 10 ** -4: - # If the move is balanced - # Add to the list of valid moves - # (analytic lines will be created later for valid moves) - valid_moves.append(move) - - # Check whether the move lines are confirmed - - if not line_draft_ids: - continue - # Update the move lines (set them as valid) - - obj_move_line.write(cr, uid, line_draft_ids, { - 'state': 'valid' - }, context, check=False) - - account = {} - account2 = {} - - if journal.type in ('purchase','sale'): - for line in move.line_id: - code = amount = 0 - key = (line.account_id.id, line.tax_code_id.id) - if key in account2: - code = account2[key][0] - amount = account2[key][1] * (line.debit + line.credit) - elif line.account_id.id in account: - code = account[line.account_id.id][0] - amount = account[line.account_id.id][1] * (line.debit + line.credit) - if (code or amount) and not (line.tax_code_id or line.tax_amount): - obj_move_line.write(cr, uid, [line.id], { - 'tax_code_id': code, - 'tax_amount': amount - }, context, check=False) - elif journal.centralisation: - # If the move is not balanced, it must be centralised... - - # Add to the list of valid moves - # (analytic lines will be created later for valid moves) - valid_moves.append(move) - - # - # Update the move lines (set them as valid) - # - self._centralise(cr, uid, move, 'debit', context=context) - self._centralise(cr, uid, move, 'credit', context=context) - obj_move_line.write(cr, uid, line_draft_ids, { - 'state': 'valid' - }, context, check=False) - else: - # We can't validate it (it's unbalanced) - # Setting the lines as draft - not_draft_line_ids = list(set(line_ids) - set(line_draft_ids)) - if not_draft_line_ids: - obj_move_line.write(cr, uid, not_draft_line_ids, { - 'state': 'draft' - }, context, check=False) - # Create analytic lines for the valid moves - for record in valid_moves: - obj_move_line.create_analytic_lines(cr, uid, [line.id for line in record.line_id], context) - - valid_moves = [move.id for move in valid_moves] - return len(valid_moves) > 0 and valid_moves or False - - -class account_move_reconcile(osv.osv): - _name = "account.move.reconcile" - _description = "Account Reconciliation" - _columns = { - 'name': fields.char('Name', required=True), - 'type': fields.char('Type', required=True), - 'line_id': fields.one2many('account.move.line', 'reconcile_id', 'Entry Lines'), - 'line_partial_ids': fields.one2many('account.move.line', 'reconcile_partial_id', 'Partial Entry lines'), - 'create_date': fields.date('Creation date', readonly=True), - 'opening_reconciliation': fields.boolean('Opening Entries Reconciliation', help="Is this reconciliation produced by the opening of a new fiscal year ?."), - } - _defaults = { - 'name': lambda self,cr,uid,ctx=None: self.pool.get('ir.sequence').next_by_code(cr, uid, 'account.reconcile', context=ctx) or '/', - } - - # You cannot unlink a reconciliation if it is a opening_reconciliation one, - # you should use the generate opening entries wizard for that - def unlink(self, cr, uid, ids, context=None): - for move_rec in self.browse(cr, uid, ids, context=context): - if move_rec.opening_reconciliation: - raise UserError(_('You cannot unreconcile journal items if they has been generated by the opening/closing fiscal year process.')) - return super(account_move_reconcile, self).unlink(cr, uid, ids, context=context) - - # Look in the line_id and line_partial_ids to ensure the partner is the same or empty - # on all lines. We allow that only for opening/closing period - def _check_same_partner(self, cr, uid, ids, context=None): - for reconcile in self.browse(cr, uid, ids, context=context): - move_lines = [] - if not reconcile.opening_reconciliation: - if reconcile.line_id: - first_partner = reconcile.line_id[0].partner_id.id - move_lines = reconcile.line_id - elif reconcile.line_partial_ids: - first_partner = reconcile.line_partial_ids[0].partner_id.id - move_lines = reconcile.line_partial_ids - if any([(line.account_id.type in ('receivable', 'payable') and line.partner_id.id != first_partner) for line in move_lines]): - return False - return True - - _constraints = [ - (_check_same_partner, 'You can only reconcile journal items with the same partner.', ['line_id', 'line_partial_ids']), - ] - - def reconcile_partial_check(self, cr, uid, ids, type='auto', context=None): - total = 0.0 - for rec in self.browse(cr, uid, ids, context=context): - for line in rec.line_partial_ids: - if line.account_id.currency_id: - total += line.amount_currency - else: - total += (line.debit or 0.0) - (line.credit or 0.0) - if not total: - self.pool.get('account.move.line').write(cr, uid, - map(lambda x: x.id, rec.line_partial_ids), - {'reconcile_id': rec.id }, - context=context - ) - return True - - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - result = [] - for r in self.browse(cr, uid, ids, context=context): - total = reduce(lambda y,t: (t.debit or 0.0) - (t.credit or 0.0) + y, r.line_partial_ids, 0.0) - if total: - name = '%s (%.2f)' % (r.name, total) - result.append((r.id,name)) - else: - result.append((r.id,r.name)) - return result + @api.multi + @api.depends('inbound_payment_methods', 'outbound_payment_methods') + def _methods_compute(self): + for journal in self: + journal.at_least_one_inbound = bool(len(self.inbound_payment_methods)) + journal.at_least_one_outbound = bool(len(self.outbound_payment_methods)) #---------------------------------------------------------- # Tax #---------------------------------------------------------- -""" -a documenter -child_depend: la taxe depend des taxes filles -""" -class account_tax_code(osv.osv): - """ - A code for the tax object. - - This code is used for some tax declarations. - """ - def _sum(self, cr, uid, ids, name, args, context, where ='', where_params=()): - parent_ids = tuple(self.search(cr, uid, [('parent_id', 'child_of', ids)])) - if context.get('based_on', 'invoices') == 'payments': - cr.execute('SELECT line.tax_code_id, sum(line.tax_amount) \ - FROM account_move_line AS line, \ - account_move AS move \ - LEFT JOIN account_invoice invoice ON \ - (invoice.move_id = move.id) \ - WHERE line.tax_code_id IN %s '+where+' \ - AND move.id = line.move_id \ - AND ((invoice.state = \'paid\') \ - OR (invoice.id IS NULL)) \ - GROUP BY line.tax_code_id', - (parent_ids,) + where_params) - else: - cr.execute('SELECT line.tax_code_id, sum(line.tax_amount) \ - FROM account_move_line AS line, \ - account_move AS move \ - WHERE line.tax_code_id IN %s '+where+' \ - AND move.id = line.move_id \ - GROUP BY line.tax_code_id', - (parent_ids,) + where_params) - res=dict(cr.fetchall()) - obj_precision = self.pool.get('decimal.precision') - res2 = {} - for record in self.browse(cr, uid, ids, context=context): - def _rec_get(record): - amount = res.get(record.id, 0.0) - for rec in record.child_ids: - amount += _rec_get(rec) * rec.sign - return amount - res2[record.id] = round(_rec_get(record), obj_precision.precision_get(cr, uid, 'Account')) - return res2 - - def _sum_year(self, cr, uid, ids, name, args, context=None): - if context is None: - context = {} - move_state = ('posted', ) - if context.get('state', 'all') == 'all': - move_state = ('draft', 'posted', ) - if context.get('fiscalyear_id', False): - fiscalyear_id = [context['fiscalyear_id']] - else: - fiscalyear_id = self.pool.get('account.fiscalyear').finds(cr, uid, exception=False) - where = '' - where_params = () - if fiscalyear_id: - pids = [] - for fy in fiscalyear_id: - pids += map(lambda x: str(x.id), self.pool.get('account.fiscalyear').browse(cr, uid, fy).period_ids) - if pids: - where = ' AND line.period_id IN %s AND move.state IN %s ' - where_params = (tuple(pids), move_state) - return self._sum(cr, uid, ids, name, args, context, - where=where, where_params=where_params) - - def _sum_period(self, cr, uid, ids, name, args, context): - if context is None: - context = {} - move_state = ('posted', ) - if context.get('state', False) == 'all': - move_state = ('draft', 'posted', ) - if context.get('period_id', False): - period_id = context['period_id'] - else: - period_id = self.pool.get('account.period').find(cr, uid, context=context) - if not period_id: - return dict.fromkeys(ids, 0.0) - period_id = period_id[0] - return self._sum(cr, uid, ids, name, args, context, - where=' AND line.period_id=%s AND move.state IN %s', where_params=(period_id, move_state)) - - _name = 'account.tax.code' - _description = 'Tax Code' - _rec_name = 'code' - _order = 'sequence, code' - _columns = { - 'name': fields.char('Tax Case Name', required=True, translate=True), - 'code': fields.char('Case Code', size=64), - 'info': fields.text('Description'), - 'sum': fields.function(_sum_year, string="Year Sum"), - 'sum_period': fields.function(_sum_period, string="Period Sum"), - 'parent_id': fields.many2one('account.tax.code', 'Parent Code', select=True), - 'child_ids': fields.one2many('account.tax.code', 'parent_id', 'Child Codes'), - 'line_ids': fields.one2many('account.move.line', 'tax_code_id', 'Lines'), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'sign': fields.float('Coefficent for parent', required=True, help='You can specify here the coefficient that will be used when consolidating the amount of this case into its parent. For example, set 1/-1 if you want to add/substract it.'), - 'notprintable':fields.boolean("Not Printable in Invoice", help="Check this box if you don't want any tax related to this tax code to appear on invoices"), - 'sequence': fields.integer('Sequence', help="Determine the display order in the report 'Accounting \ Reporting \ Generic Reporting \ Taxes \ Taxes Report'"), - } - - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): - if not args: - args = [] - if operator in expression.NEGATIVE_TERM_OPERATORS: - domain = [('code', operator, name), ('name', operator, name)] - else: - domain = ['|', ('code', operator, name), ('name', operator, name)] - ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) - return self.name_get(cr, user, ids, context=context) - - def name_get(self, cr, uid, ids, context=None): - if isinstance(ids, (int, long)): - ids = [ids] - if not ids: - return [] - if isinstance(ids, (int, long)): - ids = [ids] - reads = self.read(cr, uid, ids, ['name','code'], context=context, load='_classic_write') - return [(x['id'], (x['code'] and (x['code'] + ' - ') or '') + x['name']) \ - for x in reads] - - def _default_company(self, cr, uid, context=None): - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - if user.company_id: - return user.company_id.id - return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0] - - _defaults = { - 'company_id': _default_company, - 'sign': 1.0, - 'notprintable': False, - } - - _check_recursion = check_cycle - _constraints = [ - (_check_recursion, 'Error!\nYou cannot create recursive accounts.', ['parent_id']) - ] - _order = 'code' -def get_precision_tax(): - def change_digit_tax(cr): - res = openerp.registry(cr.dbname)['decimal.precision'].precision_get(cr, SUPERUSER_ID, 'Account') - return (16, res+3) - return change_digit_tax - -class account_tax(osv.osv): - """ - A tax object. - - Type: percent, fixed, none, code - PERCENT: tax = price * amount - FIXED: tax = price + amount - NONE: no tax line - CODE: execute python code. localcontext = {'price_unit':pu} - return result in the context - Ex: result=round(price_unit*0.21,4) - """ - def copy_data(self, cr, uid, id, default=None, context=None): - if default is None: - default = {} - this = self.browse(cr, uid, id, context=context) - tmp_default = dict(default, name=_("%s (Copy)") % this.name) - return super(account_tax, self).copy_data(cr, uid, id, default=tmp_default, context=context) - +class AccountTax(models.Model): _name = 'account.tax' _description = 'Tax' - _columns = { - 'name': fields.char('Tax Name', required=True, translate=True, help="This name will be displayed on reports"), - 'sequence': fields.integer('Sequence', required=True, help="The sequence field is used to order the tax lines from the lowest sequences to the higher ones. The order is important if you have a tax with several tax children. In this case, the evaluation order is important."), - 'amount': fields.float('Amount', required=True, digits_compute=get_precision_tax(), help="For taxes of type percentage, enter % ratio between 0-1."), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the tax without removing it."), - 'type': fields.selection( [('percent','Percentage'), ('fixed','Fixed Amount'), ('none','None'), ('code','Python Code'), ('balance','Balance')], 'Tax Type', required=True, - help="The computation method for the tax amount."), - 'applicable_type': fields.selection( [('true','Always'), ('code','Given by Python Code')], 'Applicability', required=True, - help="If not applicable (computed through a Python code), the tax won't appear on the invoice."), - 'domain':fields.char('Domain', help="This field is only used if you develop your own module allowing developers to create specific taxes in a custom domain."), - 'account_collected_id':fields.many2one('account.account', 'Invoice Tax Account', ondelete='restrict', help="Set the account that will be set by default on invoice tax lines for invoices. Leave empty to use the expense account."), - 'account_paid_id':fields.many2one('account.account', 'Refund Tax Account', ondelete='restrict', help="Set the account that will be set by default on invoice tax lines for refunds. Leave empty to use the expense account."), - 'account_analytic_collected_id':fields.many2one('account.analytic.account', 'Invoice Tax Analytic Account', help="Set the analytic account that will be used by default on the invoice tax lines for invoices. Leave empty if you don't want to use an analytic account on the invoice tax lines by default."), - 'account_analytic_paid_id':fields.many2one('account.analytic.account', 'Refund Tax Analytic Account', help="Set the analytic account that will be used by default on the invoice tax lines for refunds. Leave empty if you don't want to use an analytic account on the invoice tax lines by default."), - 'parent_id':fields.many2one('account.tax', 'Parent Tax Account', select=True), - 'child_ids':fields.one2many('account.tax', 'parent_id', 'Child Tax Accounts'), - 'child_depend':fields.boolean('Tax on Children', help="Set if the tax computation is based on the computation of child taxes rather than on the total amount."), - 'python_compute':fields.text('Python Code'), - 'python_compute_inv':fields.text('Python Code (reverse)'), - 'python_applicable':fields.text('Applicable Code'), + _order = 'sequence' - # - # Fields used for the Tax declaration - # - 'base_code_id': fields.many2one('account.tax.code', 'Account Base Code', help="Use this code for the tax declaration."), - 'tax_code_id': fields.many2one('account.tax.code', 'Account Tax Code', help="Use this code for the tax declaration."), - 'base_sign': fields.float('Base Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()), - 'tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()), + name = fields.Char(string='Tax Name', required=True, translate=True) + type_tax_use = fields.Selection([('sale', 'Sales'), ('purchase', 'Purchases'), ('none', 'None')], string='Tax Scope', required=True, default="sale", + help="Determines where the tax is selectable. Note : 'None' means a tax can't be used by itself, however it can still be used in a group.") + amount_type = fields.Selection(default='percent', string="Tax Computation", required=True, + selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')]) + active = fields.Boolean(default=True, help="Set active to false to hide the tax without removing it.") + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) + children_tax_ids = fields.Many2many('account.tax', 'account_tax_filiation_rel', 'parent_tax', 'child_tax', string='Children Taxes') + sequence = fields.Integer(required=True, default=1, + help="The sequence field is used to define order in which the tax lines are applied.") + amount = fields.Float(required=True, digits=(16, 3)) + account_id = fields.Many2one('account.account', domain=[('deprecated', '=', False)], string='Tax Account', ondelete='restrict', + help="Account that will be set on invoice tax lines for invoices. Leave empty to use the expense account.", oldname='account_collected_id') + refund_account_id = fields.Many2one('account.account', domain=[('deprecated', '=', False)], string='Tax Account on Refunds', ondelete='restrict', + help="Account that will be set on invoice tax lines for refunds. Leave empty to use the expense account.", oldname='account_paid_id') + description = fields.Char(string='Text on Invoices') + price_include = fields.Boolean(string='Included in Price', default=False, + help="Check this if the price you use on the product and invoices includes this tax.") + include_base_amount = fields.Boolean(string='Affect Subsequent Taxes', default=False, + help="If set, taxes which are computed after this one will be computed based on the price tax included.") + analytic = fields.Boolean(string="Analytic Cost", help="If set, the amount computed by this tax will be assigned to the same analytic account as the invoice line (if any)") + tag_ids = fields.Many2many('account.account.tag', 'account_tax_account_tag', string='Account tag', help="Optional tags you may want to assign for custom reporting") - # Same fields for refund invoices - - 'ref_base_code_id': fields.many2one('account.tax.code', 'Refund Base Code', help="Use this code for the tax declaration."), - 'ref_tax_code_id': fields.many2one('account.tax.code', 'Refund Tax Code', help="Use this code for the tax declaration."), - 'ref_base_sign': fields.float('Refund Base Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()), - 'ref_tax_sign': fields.float('Refund Tax Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()), - 'include_base_amount': fields.boolean('Included in base amount', help="Indicates if the amount of tax must be included in the base amount for the computation of the next taxes"), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'description': fields.char('Tax Code'), - 'price_include': fields.boolean('Tax Included in Price', help="Check this if the price you use on the product and invoices includes this tax."), - 'type_tax_use': fields.selection([('sale','Sale'),('purchase','Purchase'),('all','All')], 'Tax Application', required=True) - - } _sql_constraints = [ - ('name_company_uniq', 'unique(name, company_id)', 'Tax Name must be unique per company!'), + ('name_company_uniq', 'unique(name, company_id)', 'Tax names must be unique !'), ] - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): - """ - Returns a list of tupples containing id, name, as internally it is called {def name_get} - result format: {[(id, name), (id, name), ...]} + @api.one + @api.constrains('children_tax_ids', 'type_tax_use') + def _check_children_scope(self): + if not all(child.type_tax_use in ('none', self.type_tax_use) for child in self.children_tax_ids): + raise UserError(_('The application scope of taxes in a group must be either the same as the group or "None".')) - @param cr: A database cursor - @param user: ID of the user currently logged in - @param name: name to search - @param args: other arguments - @param operator: default operator is 'ilike', it can be changed - @param context: context arguments, like lang, time zone - @param limit: Returns first 'n' ids of complete result, default is 80. + @api.one + def copy(self, default=None): + default = dict(default or {}, name=_("%s (Copy)") % self.name) + return super(AccountTax, self).copy(default=default) - @return: Returns a list of tupples containing id and name + @api.model + def name_search(self, name, args=None, operator='ilike', limit=80): + """ Returns a list of tupples containing id, name, as internally it is called {def name_get} + result format: {[(id, name), (id, name), ...]} """ - if not args: - args = [] + args = args or [] if operator in expression.NEGATIVE_TERM_OPERATORS: domain = [('description', operator, name), ('name', operator, name)] else: domain = ['|', ('description', operator, name), ('name', operator, name)] - ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) - return self.name_get(cr, user, ids, context=context) + taxes = self.search(expression.AND([domain, args]), limit=limit) + return taxes.name_get() - def write(self, cr, uid, ids, vals, context=None): - if vals.get('type', False) and vals['type'] in ('none', 'code'): - vals.update({'amount': 0.0}) - return super(account_tax, self).write(cr, uid, ids, vals, context=context) - - def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if context is None: - context = {} - journal_pool = self.pool.get('account.journal') + @api.model + def search(self, args, offset=0, limit=None, order=None, count=False): + context = self._context or {} if context.get('type'): - if context.get('type') in ('out_invoice','out_refund'): - args += [('type_tax_use','in',['sale','all'])] - elif context.get('type') in ('in_invoice','in_refund'): - args += [('type_tax_use','in',['purchase','all'])] + if context.get('type') in ('out_invoice', 'out_refund'): + args += [('type_tax_use', '=', 'sale')] + elif context.get('type') in ('in_invoice', 'in_refund'): + args += [('type_tax_use', '=', 'purchase')] if context.get('journal_id'): - journal = journal_pool.browse(cr, uid, context.get('journal_id')) + journal = self.env['account.journal'].browse(context.get('journal_id')) if journal.type in ('sale', 'purchase'): - args += [('type_tax_use','in',[journal.type,'all'])] + args += [('type_tax_use', '=', journal.type)] - return super(account_tax, self).search(cr, uid, args, offset, limit, order, context, count) + return super(AccountTax, self).search(args, offset, limit, order, count=count) - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - res = [] - for record in self.read(cr, uid, ids, ['description','name'], context=context): - name = record['description'] and record['description'] or record['name'] - res.append((record['id'],name )) - return res + @api.onchange('amount') + def onchange_amount(self): + if self.amount_type in ('percent', 'division') and self.amount != 0.0 and not self.description: + self.description = "{0:.4g}%".format(self.amount) - def _default_company(self, cr, uid, context=None): - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - if user.company_id: - return user.company_id.id - return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0] + @api.onchange('account_id') + def onchange_account_id(self): + self.refund_account_id = self.account_id - _defaults = { - 'python_compute': '''# price_unit\n# or False\n# product: product.product object or None\n# partner: res.partner object or None\n\nresult = price_unit * 0.10''', - 'python_compute_inv': '''# price_unit\n# product: product.product object or False\n\nresult = price_unit * 0.10''', - 'applicable_type': 'true', - 'type': 'percent', - 'amount': 0, - 'price_include': 0, - 'active': 1, - 'type_tax_use': 'all', - 'sequence': 1, - 'ref_tax_sign': 1, - 'ref_base_sign': 1, - 'tax_sign': 1, - 'base_sign': 1, - 'include_base_amount': False, - 'company_id': _default_company, - } - _order = 'sequence' + @api.onchange('price_include') + def onchange_price_include(self): + if self.price_include: + self.include_base_amount = True - def _applicable(self, cr, uid, taxes, price_unit, product=None, partner=None): - res = [] - for tax in taxes: - if tax.applicable_type=='code': - localdict = {'price_unit':price_unit, 'product':product, 'partner':partner} - exec tax.python_applicable in localdict - if localdict.get('result', False): - res.append(tax) - else: - res.append(tax) - return res - - def _unit_compute(self, cr, uid, taxes, price_unit, product=None, partner=None, quantity=0): - taxes = self._applicable(cr, uid, taxes, price_unit ,product, partner) - res = [] - cur_price_unit=price_unit - for tax in taxes: - # we compute the amount for the current tax object and append it to the result - data = {'id':tax.id, - 'name':tax.description and tax.description + " - " + tax.name or tax.name, - 'account_collected_id':tax.account_collected_id.id, - 'account_paid_id':tax.account_paid_id.id, - 'account_analytic_collected_id': tax.account_analytic_collected_id.id, - 'account_analytic_paid_id': tax.account_analytic_paid_id.id, - 'base_code_id': tax.base_code_id.id, - 'ref_base_code_id': tax.ref_base_code_id.id, - 'sequence': tax.sequence, - 'base_sign': tax.base_sign, - 'tax_sign': tax.tax_sign, - 'ref_base_sign': tax.ref_base_sign, - 'ref_tax_sign': tax.ref_tax_sign, - 'price_unit': cur_price_unit, - 'tax_code_id': tax.tax_code_id.id, - 'ref_tax_code_id': tax.ref_tax_code_id.id, - } - res.append(data) - if tax.type=='percent': - amount = cur_price_unit * tax.amount - data['amount'] = amount - - elif tax.type=='fixed': - data['amount'] = tax.amount - data['tax_amount']=quantity - # data['amount'] = quantity - elif tax.type=='code': - localdict = {'price_unit':cur_price_unit, 'product':product, 'partner':partner, 'quantity': quantity} - exec tax.python_compute in localdict - amount = localdict['result'] - data['amount'] = amount - elif tax.type=='balance': - data['amount'] = cur_price_unit - reduce(lambda x,y: y.get('amount',0.0)+x, res, 0.0) - data['balance'] = cur_price_unit - - amount2 = data.get('amount', 0.0) - if tax.child_ids: - if tax.child_depend: - latest = res.pop() - amount = amount2 - child_tax = self._unit_compute(cr, uid, tax.child_ids, amount, product, partner, quantity) - res.extend(child_tax) - for child in child_tax: - amount2 += child.get('amount', 0.0) - if tax.child_depend: - for r in res: - for name in ('base','ref_base'): - if latest[name+'_code_id'] and latest[name+'_sign'] and not r[name+'_code_id']: - r[name+'_code_id'] = latest[name+'_code_id'] - r[name+'_sign'] = latest[name+'_sign'] - r['price_unit'] = latest['price_unit'] - latest[name+'_code_id'] = False - for name in ('tax','ref_tax'): - if latest[name+'_code_id'] and latest[name+'_sign'] and not r[name+'_code_id']: - r[name+'_code_id'] = latest[name+'_code_id'] - r[name+'_sign'] = latest[name+'_sign'] - r['amount'] = data['amount'] - latest[name+'_code_id'] = False - if tax.include_base_amount: - cur_price_unit+=amount2 - return res - - def compute_for_bank_reconciliation(self, cr, uid, tax_id, amount, context=None): - """ Called by RPC by the bank statement reconciliation widget """ - tax = self.browse(cr, uid, tax_id, context=context) - return self.compute_all(cr, uid, [tax], amount, 1) # TOCHECK may use force_exclude parameter - - @api.v7 - def compute_all(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None, force_excluded=False): + def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None): + """ Returns the amount of a single tax. base_amount is the actual amount on which the tax is applied, which is + price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include) """ - :param force_excluded: boolean used to say that we don't want to consider the value of field price_include of - tax. It's used in encoding by line where you don't matter if you encoded a tax with that boolean to True or - False + self.ensure_one() + if self.amount_type == 'fixed': + return math.copysign(self.amount, base_amount) * quantity + if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include): + return base_amount * self.amount / 100 + if self.amount_type == 'percent' and self.price_include: + return base_amount - (base_amount / (1 + self.amount / 100)) + if self.amount_type == 'division' and not self.price_include: + return base_amount / (1 - self.amount / 100) - base_amount + + @api.v8 + def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None): + """ Returns all information required to apply taxes (in self + their children in case of a tax goup). + We consider the sequence of the parent for group of taxes. + Eg. considering letters as taxes and alphabetic order as sequence : + [G, B([A, D, F]), E, C] will be computed as [A, D, F, C, E, G] + RETURN: { - 'total': 0.0, # Total without taxes - 'total_included: 0.0, # Total with taxes - 'taxes': [] # List of taxes, see compute for the format - } - """ - + 'total_excluded': 0.0, # Total without taxes + 'total_included': 0.0, # Total with taxes + 'taxes': [{ # One dict for each tax in self and their children + 'id': int, + 'name': str, + 'amount': float, + 'sequence': int, + 'account_id': int, + 'refund_account_id': int, + 'analytic': boolean, + }] + } """ + if not currency: + currency = self[0].company_id.currency_id + if len(self) == 0: + company_id = self.env.user.company_id + else: + company_id = self[0].company_id + taxes = [] # By default, for each tax, tax amount will first be computed # and rounded at the 'Account' decimal precision for each # PO/SO/invoice line and then these rounded amounts will be @@ -2089,1468 +456,91 @@ class account_tax(osv.osv): # precision when we round the tax amount for each line (we use # the 'Account' decimal precision + 5), and that way it's like # rounding after the sum of the tax amounts of each line - precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account') - tax_compute_precision = precision - if taxes and taxes[0].company_id.tax_calculation_rounding_method == 'round_globally': - tax_compute_precision += 5 - totalin = totalex = round(price_unit * quantity, precision) - tin = [] - tex = [] - for tax in taxes: - if not tax.price_include or force_excluded: - tex.append(tax) - else: - tin.append(tax) - tin = self.compute_inv(cr, uid, tin, price_unit, quantity, product=product, partner=partner, precision=tax_compute_precision) - for r in tin: - totalex -= r.get('amount', 0.0) - totlex_qty = 0.0 - try: - totlex_qty = totalex/quantity - except: - pass - tex = self._compute(cr, uid, tex, totlex_qty, quantity, product=product, partner=partner, precision=tax_compute_precision) - for r in tex: - totalin += r.get('amount', 0.0) - return { - 'total': totalex, - 'total_included': totalin, - 'taxes': tin + tex - } + prec = currency.decimal_places + if company_id.tax_calculation_rounding_method == 'round_globally': + prec += 5 + total_excluded = total_included = base = round(price_unit * quantity, prec) - @api.v8 - def compute_all(self, price_unit, quantity, product=None, partner=None, force_excluded=False): - return self._model.compute_all( - self._cr, self._uid, self, price_unit, quantity, - product=product, partner=partner, force_excluded=force_excluded) - - def compute(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None): - _logger.info("Deprecated, use compute_all(...)['taxes'] instead of compute(...) to manage prices with tax included.") - return self._compute(cr, uid, taxes, price_unit, quantity, product, partner) - - def _compute(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None, precision=None): - """ - Compute tax values for given PRICE_UNIT, QUANTITY and a buyer/seller ADDRESS_ID. - - RETURN: - [ tax ] - tax = {'name':'', 'amount':0.0, 'account_collected_id':1, 'account_paid_id':2} - one tax for each tax id in IDS and their children - """ - if not precision: - precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account') - res = self._unit_compute(cr, uid, taxes, price_unit, product, partner, quantity) - total = 0.0 - for r in res: - if r.get('balance',False): - r['amount'] = round(r.get('balance', 0.0) * quantity, precision) - total - else: - r['amount'] = round(r.get('amount', 0.0) * quantity, precision) - total += r['amount'] - return res - - def _unit_compute_inv(self, cr, uid, taxes, price_unit, product=None, partner=None): - taxes = self._applicable(cr, uid, taxes, price_unit, product, partner) - res = [] - taxes.reverse() - cur_price_unit = price_unit - - tax_parent_tot = 0.0 - for tax in taxes: - if (tax.type=='percent') and not tax.include_base_amount: - tax_parent_tot += tax.amount - - for tax in taxes: - if (tax.type=='fixed') and not tax.include_base_amount: - cur_price_unit -= tax.amount - - for tax in taxes: - if tax.type=='percent': - if tax.include_base_amount: - amount = cur_price_unit - (cur_price_unit / (1 + tax.amount)) - else: - amount = (cur_price_unit / (1 + tax_parent_tot)) * tax.amount - - elif tax.type=='fixed': - amount = tax.amount - - elif tax.type=='code': - localdict = {'price_unit':cur_price_unit, 'product':product, 'partner':partner} - exec tax.python_compute_inv in localdict - amount = localdict['result'] - elif tax.type=='balance': - amount = cur_price_unit - reduce(lambda x,y: y.get('amount',0.0)+x, res, 0.0) - - if tax.include_base_amount: - cur_price_unit -= amount - todo = 0 - else: - todo = 1 - res.append({ - 'id': tax.id, - 'todo': todo, - 'name': tax.name, - 'amount': amount, - 'account_collected_id': tax.account_collected_id.id, - 'account_paid_id': tax.account_paid_id.id, - 'account_analytic_collected_id': tax.account_analytic_collected_id.id, - 'account_analytic_paid_id': tax.account_analytic_paid_id.id, - 'base_code_id': tax.base_code_id.id, - 'ref_base_code_id': tax.ref_base_code_id.id, - 'sequence': tax.sequence, - 'base_sign': tax.base_sign, - 'tax_sign': tax.tax_sign, - 'ref_base_sign': tax.ref_base_sign, - 'ref_tax_sign': tax.ref_tax_sign, - 'price_unit': cur_price_unit, - 'tax_code_id': tax.tax_code_id.id, - 'ref_tax_code_id': tax.ref_tax_code_id.id, - }) - if tax.child_ids: - if tax.child_depend: - del res[-1] - amount = price_unit - - parent_tax = self._unit_compute_inv(cr, uid, tax.child_ids, amount, product, partner) - res.extend(parent_tax) - - total = 0.0 - for r in res: - if r['todo']: - total += r['amount'] - for r in res: - r['price_unit'] -= total - r['todo'] = 0 - return res - - def compute_inv(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None, precision=None): - """ - Compute tax values for given PRICE_UNIT, QUANTITY and a buyer/seller ADDRESS_ID. - Price Unit is a Tax included price - - RETURN: - [ tax ] - tax = {'name':'', 'amount':0.0, 'account_collected_id':1, 'account_paid_id':2} - one tax for each tax id in IDS and their children - """ - if not precision: - precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account') - res = self._unit_compute_inv(cr, uid, taxes, price_unit, product, partner=None) - total = 0.0 - for r in res: - if r.get('balance',False): - r['amount'] = round(r['balance'] * quantity, precision) - total - else: - r['amount'] = round(r['amount'] * quantity, precision) - total += r['amount'] - return res - - -# --------------------------------------------------------- -# Account Entries Models -# --------------------------------------------------------- - -class account_model(osv.osv): - _name = "account.model" - _description = "Account Model" - _columns = { - 'name': fields.char('Model Name', required=True, help="This is a model for recurring accounting entries"), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True), - 'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), - 'lines_id': fields.one2many('account.model.line', 'model_id', 'Model Entries', copy=True), - 'legend': fields.text('Legend', readonly=True, size=100), - } - - _defaults = { - 'legend': lambda self, cr, uid, context:_('You can specify year, month and date in the name of the model using the following labels:\n\n%(year)s: To Specify Year \n%(month)s: To Specify Month \n%(date)s: Current Date\n\ne.g. My model on %(date)s'), - } - - def generate(self, cr, uid, ids, data=None, context=None): - if data is None: - data = {} - move_ids = [] - entry = {} - account_move_obj = self.pool.get('account.move') - account_move_line_obj = self.pool.get('account.move.line') - pt_obj = self.pool.get('account.payment.term') - period_obj = self.pool.get('account.period') - - if context is None: - context = {} - - if data.get('date', False): - context = dict(context) - context.update({'date': data['date']}) - - move_date = context.get('date', time.strftime('%Y-%m-%d')) - move_date = datetime.strptime(move_date,"%Y-%m-%d") - for model in self.browse(cr, uid, ids, context=context): - ctx = context.copy() - ctx.update({'company_id': model.company_id.id}) - period_ids = period_obj.find(cr, uid, dt=context.get('date', False), context=ctx) - period_id = period_ids and period_ids[0] or False - ctx.update({'journal_id': model.journal_id.id,'period_id': period_id}) - try: - entry['name'] = model.name%{'year': move_date.strftime('%Y'), 'month': move_date.strftime('%m'), 'date': move_date.strftime('%Y-%m')} - except: - raise UserError(_('You have a wrong expression "%(...)s" in your model!')) - move_id = account_move_obj.create(cr, uid, { - 'ref': entry['name'], - 'period_id': period_id, - 'journal_id': model.journal_id.id, - 'date': context.get('date', fields.date.context_today(self,cr,uid,context=context)) - }) - move_ids.append(move_id) - for line in model.lines_id: - analytic_account_id = False - if line.analytic_account_id: - if not model.journal_id.analytic_journal_id: - raise UserError(_("You have to define an analytic journal on the '%s' journal!") % (model.journal_id.name,)) - analytic_account_id = line.analytic_account_id.id - val = { - 'move_id': move_id, - 'journal_id': model.journal_id.id, - 'period_id': period_id, - 'analytic_account_id': analytic_account_id - } - - date_maturity = context.get('date',time.strftime('%Y-%m-%d')) - if line.date_maturity == 'partner': - if not line.partner_id: - raise UserError(_("Maturity date of entry line generated by model line '%s' of model '%s' is based on partner payment term!" \ - "\nPlease define partner on it!")%(line.name, model.name)) - - payment_term_id = False - if model.journal_id.type in ('purchase', 'purchase_refund') and line.partner_id.property_supplier_payment_term: - payment_term_id = line.partner_id.property_supplier_payment_term.id - elif line.partner_id.property_payment_term: - payment_term_id = line.partner_id.property_payment_term.id - if payment_term_id: - pterm_list = pt_obj.compute(cr, uid, payment_term_id, value=1, date_ref=date_maturity) - if pterm_list: - pterm_list = [l[0] for l in pterm_list] - pterm_list.sort() - date_maturity = pterm_list[-1] - - val.update({ - 'name': line.name, - 'quantity': line.quantity, - 'debit': line.debit, - 'credit': line.credit, - 'account_id': line.account_id.id, - 'move_id': move_id, - 'partner_id': line.partner_id.id, - 'date': context.get('date', fields.date.context_today(self,cr,uid,context=context)), - 'date_maturity': date_maturity - }) - account_move_line_obj.create(cr, uid, val, context=ctx) - - return move_ids - - def onchange_journal_id(self, cr, uid, ids, journal_id, context=None): - company_id = False - - if journal_id: - journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context) - if journal.company_id.id: - company_id = journal.company_id.id - - return {'value': {'company_id': company_id}} - - -class account_model_line(osv.osv): - _name = "account.model.line" - _description = "Account Model Entries" - _columns = { - 'name': fields.char('Name', required=True), - 'sequence': fields.integer('Sequence', required=True, help="The sequence field is used to order the resources from lower sequences to higher ones."), - 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Account'), help="The optional quantity on entries."), - 'debit': fields.float('Debit', digits_compute=dp.get_precision('Account')), - 'credit': fields.float('Credit', digits_compute=dp.get_precision('Account')), - 'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade"), - 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete="cascade"), - 'model_id': fields.many2one('account.model', 'Model', required=True, ondelete="cascade", select=True), - 'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency."), - 'currency_id': fields.many2one('res.currency', 'Currency'), - 'partner_id': fields.many2one('res.partner', 'Partner'), - 'date_maturity': fields.selection([('today','Date of the day'), ('partner','Partner Payment Term')], 'Maturity Date', help="The maturity date of the generated entries for this model. You can choose between the creation date or the creation date of the entries plus the partner payment terms."), - } - _order = 'sequence' - _sql_constraints = [ - ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in model, they must be positive!'), - ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in model, they must be positive!'), - ] - -# --------------------------------------------------------- -# Account Subscription -# --------------------------------------------------------- - - -class account_subscription(osv.osv): - _name = "account.subscription" - _description = "Account Subscription" - _columns = { - 'name': fields.char('Name', required=True), - 'ref': fields.char('Reference'), - 'model_id': fields.many2one('account.model', 'Model', required=True), - 'date_start': fields.date('Start Date', required=True), - 'period_total': fields.integer('Number of Periods', required=True), - 'period_nbr': fields.integer('Period', required=True), - 'period_type': fields.selection([('day','days'),('month','month'),('year','year')], 'Period Type', required=True), - 'state': fields.selection([('draft','Draft'),('running','Running'),('done','Done')], 'Status', required=True, readonly=True, copy=False), - 'lines_id': fields.one2many('account.subscription.line', 'subscription_id', 'Subscription Lines', copy=True) - } - _defaults = { - 'date_start': fields.date.context_today, - 'period_type': 'month', - 'period_total': 12, - 'period_nbr': 1, - 'state': 'draft', - } - def state_draft(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state':'draft'}) - return False - - def check(self, cr, uid, ids, context=None): - todone = [] - for sub in self.browse(cr, uid, ids, context=context): - ok = True - for line in sub.lines_id: - if not line.move_id.id: - ok = False - break - if ok: - todone.append(sub.id) - if todone: - self.write(cr, uid, todone, {'state':'done'}) - return False - - def remove_line(self, cr, uid, ids, context=None): - toremove = [] - for sub in self.browse(cr, uid, ids, context=context): - for line in sub.lines_id: - if not line.move_id.id: - toremove.append(line.id) - if toremove: - self.pool.get('account.subscription.line').unlink(cr, uid, toremove) - self.write(cr, uid, ids, {'state':'draft'}) - return False - - def compute(self, cr, uid, ids, context=None): - for sub in self.browse(cr, uid, ids, context=context): - ds = sub.date_start - for i in range(sub.period_total): - self.pool.get('account.subscription.line').create(cr, uid, { - 'date': ds, - 'subscription_id': sub.id, - }) - if sub.period_type=='day': - ds = (datetime.strptime(ds, '%Y-%m-%d') + relativedelta(days=sub.period_nbr)).strftime('%Y-%m-%d') - if sub.period_type=='month': - ds = (datetime.strptime(ds, '%Y-%m-%d') + relativedelta(months=sub.period_nbr)).strftime('%Y-%m-%d') - if sub.period_type=='year': - ds = (datetime.strptime(ds, '%Y-%m-%d') + relativedelta(years=sub.period_nbr)).strftime('%Y-%m-%d') - self.write(cr, uid, ids, {'state':'running'}) - return True - - -class account_subscription_line(osv.osv): - _name = "account.subscription.line" - _description = "Account Subscription Line" - _columns = { - 'subscription_id': fields.many2one('account.subscription', 'Subscription', required=True, select=True), - 'date': fields.date('Date', required=True), - 'move_id': fields.many2one('account.move', 'Entry'), - } - - def move_create(self, cr, uid, ids, context=None): - tocheck = {} - all_moves = [] - obj_model = self.pool.get('account.model') - for line in self.browse(cr, uid, ids, context=context): - data = { - 'date': line.date, - } - move_ids = obj_model.generate(cr, uid, [line.subscription_id.model_id.id], data, context) - tocheck[line.subscription_id.id] = True - self.write(cr, uid, [line.id], {'move_id':move_ids[0]}) - all_moves.extend(move_ids) - if tocheck: - self.pool.get('account.subscription').check(cr, uid, tocheck.keys(), context) - return all_moves - - _rec_name = 'date' - - -# --------------------------------------------------------------- -# Account Templates: Account, Tax, Tax Code and chart. + Wizard -# --------------------------------------------------------------- - -class account_tax_template(osv.osv): - _name = 'account.tax.template' - -class account_account_template(osv.osv): - _order = "code" - _name = "account.account.template" - _description ='Templates for Accounts' - - _columns = { - 'name': fields.char('Name', required=True, select=True), - 'currency_id': fields.many2one('res.currency', 'Secondary Currency', help="Forces all moves for this account to have this secondary currency."), - 'code': fields.char('Code', size=64, required=True, select=1), - 'type': fields.selection([ - ('receivable','Receivable'), - ('payable','Payable'), - ('view','View'), - ('consolidation','Consolidation'), - ('liquidity','Liquidity'), - ('other','Regular'), - ('closed','Closed'), - ], 'Internal Type', required=True,help="This type is used to differentiate types with "\ - "special effects in Odoo: view can not have entries, consolidation are accounts that "\ - "can have children accounts for multi-company consolidations, payable/receivable are for "\ - "partners accounts (for debit/credit computations), closed for depreciated accounts."), - 'user_type': fields.many2one('account.account.type', 'Account Type', required=True, - help="These types are defined according to your country. The type contains more information "\ - "about the account and its specificities."), - 'financial_report_ids': fields.many2many('account.financial.report', 'account_template_financial_report', 'account_template_id', 'report_line_id', 'Financial Reports'), - 'reconcile': fields.boolean('Allow Reconciliation', help="Check this option if you want the user to reconcile entries in this account."), - 'shortcut': fields.char('Shortcut', size=12), - 'note': fields.text('Note'), - 'parent_id': fields.many2one('account.account.template', 'Parent Account Template', ondelete='cascade', domain=[('type','=','view')]), - 'child_parent_ids':fields.one2many('account.account.template', 'parent_id', 'Children'), - 'tax_ids': fields.many2many('account.tax.template', 'account_account_template_tax_rel', 'account_id', 'tax_id', 'Default Taxes'), - 'nocreate': fields.boolean('Optional create', help="If checked, the new chart of accounts will not contain this by default."), - 'chart_template_id': fields.many2one('account.chart.template', 'Chart Template', help="This optional field allow you to link an account template to a specific chart template that may differ from the one its root parent belongs to. This allow you to define chart templates that extend another and complete it with few new accounts (You don't need to define the whole structure that is common to both several times)."), - } - - _defaults = { - 'reconcile': False, - 'type': 'view', - 'nocreate': False, - } - - _check_recursion = check_cycle - _constraints = [ - (_check_recursion, 'Error!\nYou cannot create recursive account templates.', ['parent_id']), - ] - - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - reads = self.read(cr, uid, ids, ['name','code'], context=context) - res = [] - for record in reads: - name = record['name'] - if record['code']: - name = record['code']+' '+name - res.append((record['id'],name )) - return res - - def generate_account(self, cr, uid, chart_template_id, tax_template_ref, acc_template_ref, code_digits, company_id, context=None): - """ - This method for generating accounts from templates. - - :param chart_template_id: id of the chart template chosen in the wizard - :param tax_template_ref: Taxes templates reference for write taxes_id in account_account. - :paramacc_template_ref: dictionary with the mappping between the account templates and the real accounts. - :param code_digits: number of digits got from wizard.multi.charts.accounts, this is use for account code. - :param company_id: company_id selected from wizard.multi.charts.accounts. - :returns: return acc_template_ref for reference purpose. - :rtype: dict - """ - if context is None: - context = {} - obj_acc = self.pool.get('account.account') - company_name = self.pool.get('res.company').browse(cr, uid, company_id, context=context).name - template = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context) - #deactivate the parent_store functionnality on account_account for rapidity purpose - ctx = context.copy() - ctx.update({'defer_parent_store_computation': True}) - level_ref = {} - children_acc_criteria = [('chart_template_id','=', chart_template_id)] - if template.account_root_id.id: - children_acc_criteria = ['|'] + children_acc_criteria + ['&',('parent_id','child_of', [template.account_root_id.id]),('chart_template_id','=', False)] - children_acc_template = self.search(cr, uid, [('nocreate','!=',True)] + children_acc_criteria, order='id') - for account_template in self.browse(cr, uid, children_acc_template, context=context): - # skip the root of COA if it's not the main one - if (template.account_root_id.id == account_template.id) and template.parent_id: + for tax in self: + if tax.amount_type == 'group': + ret = tax.children_tax_ids.compute_all(price_unit, currency, quantity, product, partner) + total_excluded = ret['total_excluded'] + base = ret['total_excluded'] + total_included = ret['total_included'] + tax_amount = total_included - total_excluded + taxes += ret['taxes'] continue - tax_ids = [] - for tax in account_template.tax_ids: - tax_ids.append(tax_template_ref[tax.id]) - code_main = account_template.code and len(account_template.code) or 0 - code_acc = account_template.code or '' - if code_main > 0 and code_main <= code_digits and account_template.type != 'view': - code_acc = str(code_acc) + (str('0'*(code_digits-code_main))) - parent_id = account_template.parent_id and ((account_template.parent_id.id in acc_template_ref) and acc_template_ref[account_template.parent_id.id]) or False - #the level as to be given as well at the creation time, because of the defer_parent_store_computation in - #context. Indeed because of this, the parent_left and parent_right are not computed and thus the child_of - #operator does not return the expected values, with result of having the level field not computed at all. - if parent_id: - level = parent_id in level_ref and level_ref[parent_id] + 1 or obj_acc._get_level(cr, uid, [parent_id], 'level', None, context=context)[parent_id] + 1 - else: - level = 0 - vals={ - 'name': (template.account_root_id.id == account_template.id) and company_name or account_template.name, - 'currency_id': account_template.currency_id and account_template.currency_id.id or False, - 'code': code_acc, - 'type': account_template.type, - 'user_type': account_template.user_type and account_template.user_type.id or False, - 'reconcile': account_template.reconcile, - 'shortcut': account_template.shortcut, - 'note': account_template.note, - 'financial_report_ids': account_template.financial_report_ids and [(6,0,[x.id for x in account_template.financial_report_ids])] or False, - 'parent_id': parent_id, - 'tax_ids': [(6,0,tax_ids)], - 'company_id': company_id, - 'level': level, - } - new_account = obj_acc.create(cr, uid, vals, context=ctx) - acc_template_ref[account_template.id] = new_account - level_ref[new_account] = level + tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner) + tax_amount = currency.round(tax_amount) - #reactivate the parent_store functionnality on account_account - obj_acc._parent_store_compute(cr) - return acc_template_ref - - -class account_add_tmpl_wizard(osv.osv_memory): - """Add one more account from the template. - - With the 'nocreate' option, some accounts may not be created. Use this to add them later.""" - _name = 'account.addtmpl.wizard' - - def _get_def_cparent(self, cr, uid, context=None): - acc_obj = self.pool.get('account.account') - tmpl_obj = self.pool.get('account.account.template') - tids = tmpl_obj.read(cr, uid, [context['tmpl_ids']], ['parent_id']) - if not tids or not tids[0]['parent_id']: - return False - ptids = tmpl_obj.read(cr, uid, [tids[0]['parent_id'][0]], ['code']) - res = None - if not ptids or not ptids[0]['code']: - raise UserError(_('There is no parent code for the template account.')) - res = acc_obj.search(cr, uid, [('code','=',ptids[0]['code'])]) - return res and res[0] or False - - _columns = { - 'cparent_id':fields.many2one('account.account', 'Parent target', help="Creates an account with the selected template under this existing parent.", required=True), - } - _defaults = { - 'cparent_id': _get_def_cparent, - } - - def action_create(self,cr,uid,ids,context=None): - if context is None: - context = {} - acc_obj = self.pool.get('account.account') - tmpl_obj = self.pool.get('account.account.template') - data = self.read(cr, uid, ids)[0] - company_id = acc_obj.read(cr, uid, [data['cparent_id'][0]], ['company_id'])[0]['company_id'][0] - account_template = tmpl_obj.browse(cr, uid, context['tmpl_ids']) - vals = { - 'name': account_template.name, - 'currency_id': account_template.currency_id and account_template.currency_id.id or False, - 'code': account_template.code, - 'type': account_template.type, - 'user_type': account_template.user_type and account_template.user_type.id or False, - 'reconcile': account_template.reconcile, - 'shortcut': account_template.shortcut, - 'note': account_template.note, - 'parent_id': data['cparent_id'][0], - 'company_id': company_id, - } - acc_obj.create(cr, uid, vals) - return {'type':'state', 'state': 'end' } - - def action_cancel(self, cr, uid, ids, context=None): - return { 'type': 'state', 'state': 'end' } - - -class account_tax_code_template(osv.osv): - - _name = 'account.tax.code.template' - _description = 'Tax Code Template' - _order = 'sequence, code' - _rec_name = 'code' - _columns = { - 'name': fields.char('Tax Case Name', required=True), - 'code': fields.char('Case Code', size=64), - 'info': fields.text('Description'), - 'parent_id': fields.many2one('account.tax.code.template', 'Parent Code', select=True), - 'child_ids': fields.one2many('account.tax.code.template', 'parent_id', 'Child Codes'), - 'sign': fields.float('Sign For Parent', required=True), - 'notprintable':fields.boolean("Not Printable in Invoice", help="Check this box if you don't want any tax related to this tax Code to appear on invoices."), - 'sequence': fields.integer( - 'Sequence', help=( - "Determine the display order in the report 'Accounting " - "\ Reporting \ Generic Reporting \ Taxes \ Taxes Report'"), - ), - } - - _defaults = { - 'sign': 1.0, - 'notprintable': False, - } - - def generate_tax_code(self, cr, uid, tax_code_root_id, company_id, context=None): - ''' - This function generates the tax codes from the templates of tax code that are children of the given one passed - in argument. Then it returns a dictionary with the mappping between the templates and the real objects. - - :param tax_code_root_id: id of the root of all the tax code templates to process - :param company_id: id of the company the wizard is running for - :returns: dictionary with the mappping between the templates and the real objects. - :rtype: dict - ''' - obj_tax_code_template = self.pool.get('account.tax.code.template') - obj_tax_code = self.pool.get('account.tax.code') - tax_code_template_ref = {} - company = self.pool.get('res.company').browse(cr, uid, company_id, context=context) - - #find all the children of the tax_code_root_id - children_tax_code_template = tax_code_root_id and obj_tax_code_template.search(cr, uid, [('parent_id','child_of',[tax_code_root_id])], order='id') or [] - for tax_code_template in obj_tax_code_template.browse(cr, uid, children_tax_code_template, context=context): - vals = { - 'name': (tax_code_root_id == tax_code_template.id) and company.name or tax_code_template.name, - 'code': tax_code_template.code, - 'info': tax_code_template.info, - 'parent_id': tax_code_template.parent_id and ((tax_code_template.parent_id.id in tax_code_template_ref) and tax_code_template_ref[tax_code_template.parent_id.id]) or False, - 'company_id': company_id, - 'sign': tax_code_template.sign, - 'sequence': tax_code_template.sequence, - } - #check if this tax code already exists - rec_list = obj_tax_code.search(cr, uid, [('name', '=', vals['name']),('code', '=', vals['code']),('company_id', '=', vals['company_id'])], context=context) - if not rec_list: - #if not yet, create it - new_tax_code = obj_tax_code.create(cr, uid, vals) - #recording the new tax code to do the mapping - tax_code_template_ref[tax_code_template.id] = new_tax_code - return tax_code_template_ref - - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - if isinstance(ids, (int, long)): - ids = [ids] - reads = self.read(cr, uid, ids, ['name','code'], context=context, load='_classic_write') - return [(x['id'], (x['code'] and x['code'] + ' - ' or '') + x['name']) \ - for x in reads] - - _check_recursion = check_cycle - _constraints = [ - (_check_recursion, 'Error!\nYou cannot create recursive Tax Codes.', ['parent_id']) - ] - _order = 'code,name' - - -class account_chart_template(osv.osv): - _name="account.chart.template" - _description= "Templates for Account Chart" - - _columns={ - 'name': fields.char('Name', required=True), - 'parent_id': fields.many2one('account.chart.template', 'Parent Chart Template'), - 'code_digits': fields.integer('# of Digits', required=True, help="No. of Digits to use for account code"), - 'visible': fields.boolean('Can be Visible?', help="Set this to False if you don't want this template to be used actively in the wizard that generate Chart of Accounts from templates, this is useful when you want to generate accounts of this template only when loading its child template."), - 'currency_id': fields.many2one('res.currency', 'Currency'), - 'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sale and purchase rates or choose from list of taxes. This last choice assumes that the set of tax defined on this template is complete'), - 'account_root_id': fields.many2one('account.account.template', 'Root Account', domain=[('parent_id','=',False)]), - 'tax_code_root_id': fields.many2one('account.tax.code.template', 'Root Tax Code', domain=[('parent_id','=',False)]), - 'tax_template_ids': fields.one2many('account.tax.template', 'chart_template_id', 'Tax Template List', help='List of all the taxes that have to be installed by the wizard'), - 'bank_account_view_id': fields.many2one('account.account.template', 'Bank Account'), - 'property_account_receivable': fields.many2one('account.account.template', 'Receivable Account'), - 'property_account_payable': fields.many2one('account.account.template', 'Payable Account'), - 'property_account_expense_categ': fields.many2one('account.account.template', 'Expense Category Account'), - 'property_account_income_categ': fields.many2one('account.account.template', 'Income Category Account'), - 'property_account_expense': fields.many2one('account.account.template', 'Expense Account on Product Template'), - 'property_account_income': fields.many2one('account.account.template', 'Income Account on Product Template'), - 'property_account_income_opening': fields.many2one('account.account.template', 'Opening Entries Income Account'), - 'property_account_expense_opening': fields.many2one('account.account.template', 'Opening Entries Expense Account'), - } - - _defaults = { - 'visible': True, - 'code_digits': 6, - 'complete_tax_set': True, - } - - -class account_tax_template(osv.osv): - - _name = 'account.tax.template' - _description = 'Templates for Taxes' - - _columns = { - 'chart_template_id': fields.many2one('account.chart.template', 'Chart Template', required=True), - 'name': fields.char('Tax Name', required=True), - 'sequence': fields.integer('Sequence', required=True, help="The sequence field is used to order the taxes lines from lower sequences to higher ones. The order is important if you have a tax that has several tax children. In this case, the evaluation order is important."), - 'amount': fields.float('Amount', required=True, digits_compute=get_precision_tax(), help="For Tax Type percent enter % ratio between 0-1."), - 'type': fields.selection( [('percent','Percent'), ('fixed','Fixed'), ('none','None'), ('code','Python Code'), ('balance','Balance')], 'Tax Type', required=True), - 'applicable_type': fields.selection( [('true','True'), ('code','Python Code')], 'Applicable Type', required=True, help="If not applicable (computed through a Python code), the tax won't appear on the invoice."), - 'domain':fields.char('Domain', help="This field is only used if you develop your own module allowing developers to create specific taxes in a custom domain."), - 'account_collected_id':fields.many2one('account.account.template', 'Invoice Tax Account'), - 'account_paid_id':fields.many2one('account.account.template', 'Refund Tax Account'), - 'parent_id':fields.many2one('account.tax.template', 'Parent Tax Account', select=True), - 'child_depend':fields.boolean('Tax on Children', help="Set if the tax computation is based on the computation of child taxes rather than on the total amount."), - 'python_compute':fields.text('Python Code'), - 'python_compute_inv':fields.text('Python Code (reverse)'), - 'python_applicable':fields.text('Applicable Code'), - - # - # Fields used for the Tax declaration - # - 'base_code_id': fields.many2one('account.tax.code.template', 'Base Code', help="Use this code for the tax declaration."), - 'tax_code_id': fields.many2one('account.tax.code.template', 'Tax Code', help="Use this code for the tax declaration."), - 'base_sign': fields.float('Base Code Sign', help="Usually 1 or -1."), - 'tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1."), - - # Same fields for refund invoices - - 'ref_base_code_id': fields.many2one('account.tax.code.template', 'Refund Base Code', help="Use this code for the tax declaration."), - 'ref_tax_code_id': fields.many2one('account.tax.code.template', 'Refund Tax Code', help="Use this code for the tax declaration."), - 'ref_base_sign': fields.float('Refund Base Code Sign', help="Usually 1 or -1."), - 'ref_tax_sign': fields.float('Refund Tax Code Sign', help="Usually 1 or -1."), - 'include_base_amount': fields.boolean('Include in Base Amount', help="Set if the amount of tax must be included in the base amount before computing the next taxes."), - 'description': fields.char('Internal Name'), - 'type_tax_use': fields.selection([('sale','Sale'),('purchase','Purchase'),('all','All')], 'Tax Use In', required=True,), - 'price_include': fields.boolean('Tax Included in Price', help="Check this if the price you use on the product and invoices includes this tax."), - } - - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - res = [] - for record in self.read(cr, uid, ids, ['description','name'], context=context): - name = record['description'] and record['description'] or record['name'] - res.append((record['id'],name )) - return res - - def _default_company(self, cr, uid, context=None): - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - if user.company_id: - return user.company_id.id - return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0] - - _defaults = { - 'python_compute': lambda *a: '''# price_unit\n# product: product.product object or None\n# partner: res.partner object or None\n\nresult = price_unit * 0.10''', - 'python_compute_inv': lambda *a: '''# price_unit\n# product: product.product object or False\n\nresult = price_unit * 0.10''', - 'applicable_type': 'true', - 'type': 'percent', - 'amount': 0, - 'sequence': 1, - 'ref_tax_sign': 1, - 'ref_base_sign': 1, - 'tax_sign': 1, - 'base_sign': 1, - 'include_base_amount': False, - 'type_tax_use': 'all', - 'price_include': 0, - } - _order = 'sequence' - - def _generate_tax(self, cr, uid, tax_templates, tax_code_template_ref, company_id, context=None): - """ - This method generate taxes from templates. - - :param tax_templates: list of browse record of the tax templates to process - :param tax_code_template_ref: Taxcode templates reference. - :param company_id: id of the company the wizard is running for - :returns: - { - 'tax_template_to_tax': mapping between tax template and the newly generated taxes corresponding, - 'account_dict': dictionary containing a to-do list with all the accounts to assign on new taxes - } - """ - if context is None: - context = {} - res = {} - todo_dict = {} - tax_template_to_tax = {} - for tax in tax_templates: - vals_tax = { - 'name':tax.name, - 'sequence': tax.sequence, - 'amount': tax.amount, - 'type': tax.type, - 'applicable_type': tax.applicable_type, - 'domain': tax.domain, - 'parent_id': tax.parent_id and ((tax.parent_id.id in tax_template_to_tax) and tax_template_to_tax[tax.parent_id.id]) or False, - 'child_depend': tax.child_depend, - 'python_compute': tax.python_compute, - 'python_compute_inv': tax.python_compute_inv, - 'python_applicable': tax.python_applicable, - 'base_code_id': tax.base_code_id and ((tax.base_code_id.id in tax_code_template_ref) and tax_code_template_ref[tax.base_code_id.id]) or False, - 'tax_code_id': tax.tax_code_id and ((tax.tax_code_id.id in tax_code_template_ref) and tax_code_template_ref[tax.tax_code_id.id]) or False, - 'base_sign': tax.base_sign, - 'tax_sign': tax.tax_sign, - 'ref_base_code_id': tax.ref_base_code_id and ((tax.ref_base_code_id.id in tax_code_template_ref) and tax_code_template_ref[tax.ref_base_code_id.id]) or False, - 'ref_tax_code_id': tax.ref_tax_code_id and ((tax.ref_tax_code_id.id in tax_code_template_ref) and tax_code_template_ref[tax.ref_tax_code_id.id]) or False, - 'ref_base_sign': tax.ref_base_sign, - 'ref_tax_sign': tax.ref_tax_sign, - 'include_base_amount': tax.include_base_amount, - 'description': tax.description, - 'company_id': company_id, - 'type_tax_use': tax.type_tax_use, - 'price_include': tax.price_include - } - new_tax = self.pool.get('account.tax').create(cr, uid, vals_tax) - tax_template_to_tax[tax.id] = new_tax - #as the accounts have not been created yet, we have to wait before filling these fields - todo_dict[new_tax] = { - 'account_collected_id': tax.account_collected_id and tax.account_collected_id.id or False, - 'account_paid_id': tax.account_paid_id and tax.account_paid_id.id or False, - } - res.update({'tax_template_to_tax': tax_template_to_tax, 'account_dict': todo_dict}) - return res - - -# Fiscal Position Templates - -class account_fiscal_position_template(osv.osv): - _name = 'account.fiscal.position.template' - _description = 'Template for Fiscal Position' - - _columns = { - 'name': fields.char('Fiscal Position Template', required=True), - 'chart_template_id': fields.many2one('account.chart.template', 'Chart Template', required=True), - 'account_ids': fields.one2many('account.fiscal.position.account.template', 'position_id', 'Account Mapping'), - 'tax_ids': fields.one2many('account.fiscal.position.tax.template', 'position_id', 'Tax Mapping'), - 'note': fields.text('Notes'), - } - - def generate_fiscal_position(self, cr, uid, chart_temp_id, tax_template_ref, acc_template_ref, company_id, context=None): - """ - This method generate Fiscal Position, Fiscal Position Accounts and Fiscal Position Taxes from templates. - - :param chart_temp_id: Chart Template Id. - :param taxes_ids: Taxes templates reference for generating account.fiscal.position.tax. - :param acc_template_ref: Account templates reference for generating account.fiscal.position.account. - :param company_id: company_id selected from wizard.multi.charts.accounts. - :returns: True - """ - if context is None: - context = {} - obj_tax_fp = self.pool.get('account.fiscal.position.tax') - obj_ac_fp = self.pool.get('account.fiscal.position.account') - obj_fiscal_position = self.pool.get('account.fiscal.position') - fp_ids = self.search(cr, uid, [('chart_template_id', '=', chart_temp_id)]) - for position in self.browse(cr, uid, fp_ids, context=context): - new_fp = obj_fiscal_position.create(cr, uid, {'company_id': company_id, 'name': position.name, 'note': position.note}) - for tax in position.tax_ids: - obj_tax_fp.create(cr, uid, { - 'tax_src_id': tax_template_ref[tax.tax_src_id.id], - 'tax_dest_id': tax.tax_dest_id and tax_template_ref[tax.tax_dest_id.id] or False, - 'position_id': new_fp - }) - for acc in position.account_ids: - obj_ac_fp.create(cr, uid, { - 'account_src_id': acc_template_ref[acc.account_src_id.id], - 'account_dest_id': acc_template_ref[acc.account_dest_id.id], - 'position_id': new_fp - }) - return True - - -class account_fiscal_position_tax_template(osv.osv): - _name = 'account.fiscal.position.tax.template' - _description = 'Template Tax Fiscal Position' - _rec_name = 'position_id' - - _columns = { - 'position_id': fields.many2one('account.fiscal.position.template', 'Fiscal Position', required=True, ondelete='cascade'), - 'tax_src_id': fields.many2one('account.tax.template', 'Tax Source', required=True), - 'tax_dest_id': fields.many2one('account.tax.template', 'Replacement Tax') - } - - -class account_fiscal_position_account_template(osv.osv): - _name = 'account.fiscal.position.account.template' - _description = 'Template Account Fiscal Mapping' - _rec_name = 'position_id' - _columns = { - 'position_id': fields.many2one('account.fiscal.position.template', 'Fiscal Mapping', required=True, ondelete='cascade'), - 'account_src_id': fields.many2one('account.account.template', 'Account Source', domain=[('type','<>','view')], required=True), - 'account_dest_id': fields.many2one('account.account.template', 'Account Destination', domain=[('type','<>','view')], required=True) - } - - -# --------------------------------------------------------- -# Account generation from template wizards -# --------------------------------------------------------- - -class wizard_multi_charts_accounts(osv.osv_memory): - """ - Create a new account chart for a company. - Wizards ask for: - * a company - * an account chart template - * a number of digits for formatting code of non-view accounts - * a list of bank accounts owned by the company - Then, the wizard: - * generates all accounts from the template and assigns them to the right company - * generates all taxes and tax codes, changing account assignations - * generates all accounting properties and assigns them correctly - """ - _name='wizard.multi.charts.accounts' - _inherit = 'res.config' - - _columns = { - 'company_id':fields.many2one('res.company', 'Company', required=True), - 'currency_id': fields.many2one('res.currency', 'Currency', help="Currency as per company's country."), - 'only_one_chart_template': fields.boolean('Only One Chart Template Available'), - 'chart_template_id': fields.many2one('account.chart.template', 'Chart Template', required=True), - 'bank_accounts_id': fields.one2many('account.bank.accounts.wizard', 'bank_account_id', 'Cash and Banks', required=True), - 'code_digits':fields.integer('# of Digits', required=True, help="No. of Digits to use for account code"), - "sale_tax": fields.many2one("account.tax.template", "Default Sale Tax"), - "purchase_tax": fields.many2one("account.tax.template", "Default Purchase Tax"), - 'sale_tax_rate': fields.float('Sales Tax(%)'), - 'purchase_tax_rate': fields.float('Purchase Tax(%)'), - 'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete'), - } - - - def _get_chart_parent_ids(self, cr, uid, chart_template, context=None): - """ Returns the IDs of all ancestor charts, including the chart itself. - (inverse of child_of operator) - - :param browse_record chart_template: the account.chart.template record - :return: the IDS of all ancestor charts, including the chart itself. - """ - result = [chart_template.id] - while chart_template.parent_id: - chart_template = chart_template.parent_id - result.append(chart_template.id) - return result - - def onchange_tax_rate(self, cr, uid, ids, rate=False, context=None): - return {'value': {'purchase_tax_rate': rate or False}} - - def onchange_chart_template_id(self, cr, uid, ids, chart_template_id=False, context=None): - res = {} - tax_templ_obj = self.pool.get('account.tax.template') - res['value'] = {'complete_tax_set': False, 'sale_tax': False, 'purchase_tax': False} - if chart_template_id: - data = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context) - currency_id = data.currency_id and data.currency_id.id or self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id - res['value'].update({'complete_tax_set': data.complete_tax_set, 'currency_id': currency_id}) - if data.complete_tax_set: - # default tax is given by the lowest sequence. For same sequence we will take the latest created as it will be the case for tax created while isntalling the generic chart of account - chart_ids = self._get_chart_parent_ids(cr, uid, data, context=context) - base_tax_domain = [("chart_template_id", "in", chart_ids), ('parent_id', '=', False)] - sale_tax_domain = base_tax_domain + [('type_tax_use', 'in', ('sale','all'))] - purchase_tax_domain = base_tax_domain + [('type_tax_use', 'in', ('purchase','all'))] - sale_tax_ids = tax_templ_obj.search(cr, uid, sale_tax_domain, order="sequence, id desc") - purchase_tax_ids = tax_templ_obj.search(cr, uid, purchase_tax_domain, order="sequence, id desc") - res['value'].update({'sale_tax': sale_tax_ids and sale_tax_ids[0] or False, - 'purchase_tax': purchase_tax_ids and purchase_tax_ids[0] or False}) - res.setdefault('domain', {}) - res['domain']['sale_tax'] = repr(sale_tax_domain) - res['domain']['purchase_tax'] = repr(purchase_tax_domain) - if data.code_digits: - res['value'].update({'code_digits': data.code_digits}) - return res - - def default_get(self, cr, uid, fields, context=None): - res = super(wizard_multi_charts_accounts, self).default_get(cr, uid, fields, context=context) - tax_templ_obj = self.pool.get('account.tax.template') - account_chart_template = self.pool['account.chart.template'] - - if 'bank_accounts_id' in fields: - res.update({'bank_accounts_id': [{'acc_name': _('Cash'), 'account_type': 'cash'},{'acc_name': _('Bank'), 'account_type': 'bank'}]}) - if 'company_id' in fields: - res.update({'company_id': self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0].company_id.id}) - if 'currency_id' in fields: - company_id = res.get('company_id') or False - if company_id: - company_obj = self.pool.get('res.company') - country_id = company_obj.browse(cr, uid, company_id, context=context).country_id.id - currency_id = company_obj.on_change_country(cr, uid, company_id, country_id, context=context)['value']['currency_id'] - res.update({'currency_id': currency_id}) - - ids = account_chart_template.search(cr, uid, [('visible', '=', True)], context=context) - if ids: - #in order to set default chart which was last created set max of ids. - chart_id = max(ids) - if context.get("default_charts"): - model_data = self.pool.get('ir.model.data').search_read(cr, uid, [('model','=','account.chart.template'),('module','=',context.get("default_charts"))], ['res_id'], context=context) - if model_data: - chart_id = model_data[0]['res_id'] - chart = account_chart_template.browse(cr, uid, chart_id, context=context) - chart_hierarchy_ids = self._get_chart_parent_ids(cr, uid, chart, context=context) - if 'chart_template_id' in fields: - res.update({'only_one_chart_template': len(ids) == 1, - 'chart_template_id': chart_id}) - if 'sale_tax' in fields: - sale_tax_ids = tax_templ_obj.search(cr, uid, [("chart_template_id", "in", chart_hierarchy_ids), - ('type_tax_use', 'in', ('sale','all'))], - order="sequence") - res.update({'sale_tax': sale_tax_ids and sale_tax_ids[0] or False}) - if 'purchase_tax' in fields: - purchase_tax_ids = tax_templ_obj.search(cr, uid, [("chart_template_id", "in", chart_hierarchy_ids), - ('type_tax_use', 'in', ('purchase','all'))], - order="sequence") - res.update({'purchase_tax': purchase_tax_ids and purchase_tax_ids[0] or False}) - res.update({ - 'purchase_tax_rate': 15.0, - 'sale_tax_rate': 15.0, - }) - return res - - def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - if context is None:context = {} - res = super(wizard_multi_charts_accounts, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar,submenu=False) - cmp_select = [] - acc_template_obj = self.pool.get('account.chart.template') - company_obj = self.pool.get('res.company') - - company_ids = company_obj.search(cr, uid, [], context=context) - #display in the widget selection of companies, only the companies that haven't been configured yet (but don't care about the demo chart of accounts) - cr.execute("SELECT company_id FROM account_account WHERE active = 't' AND account_account.parent_id IS NULL AND name != %s", ("Chart For Automated Tests",)) - configured_cmp = [r[0] for r in cr.fetchall()] - unconfigured_cmp = list(set(company_ids)-set(configured_cmp)) - for field in res['fields']: - if field == 'company_id': - res['fields'][field]['domain'] = [('id','in',unconfigured_cmp)] - res['fields'][field]['selection'] = [('', '')] - if unconfigured_cmp: - cmp_select = [(line.id, line.name) for line in company_obj.browse(cr, uid, unconfigured_cmp)] - res['fields'][field]['selection'] = cmp_select - return res - - def check_created_journals(self, cr, uid, vals_journal, company_id, context=None): - """ - This method used for checking journals already created or not. If not then create new journal. - """ - obj_journal = self.pool.get('account.journal') - rec_list = obj_journal.search(cr, uid, [('name','=', vals_journal['name']),('company_id', '=', company_id)], context=context) - if not rec_list: - obj_journal.create(cr, uid, vals_journal, context=context) - return True - - def generate_journals(self, cr, uid, chart_template_id, acc_template_ref, company_id, context=None): - """ - This method is used for creating journals. - - :param chart_temp_id: Chart Template Id. - :param acc_template_ref: Account templates reference. - :param company_id: company_id selected from wizard.multi.charts.accounts. - :returns: True - """ - journal_data = self._prepare_all_journals(cr, uid, chart_template_id, acc_template_ref, company_id, context=context) - for vals_journal in journal_data: - self.check_created_journals(cr, uid, vals_journal, company_id, context=context) - return True - - def _prepare_all_journals(self, cr, uid, chart_template_id, acc_template_ref, company_id, context=None): - def _get_analytic_journal(journal_type): - # Get the analytic journal - data = False - try: - if journal_type in ('sale', 'sale_refund'): - data = obj_data.get_object_reference(cr, uid, 'account', 'analytic_journal_sale') - elif journal_type in ('purchase', 'purchase_refund'): - data = obj_data.get_object_reference(cr, uid, 'account', 'exp') - elif journal_type == 'general': - pass - except ValueError: - pass - return data and data[1] or False - - def _get_default_account(journal_type, type='debit'): - # Get the default accounts - default_account = False - if journal_type in ('sale', 'sale_refund'): - default_account = acc_template_ref.get(template.property_account_income_categ.id) - elif journal_type in ('purchase', 'purchase_refund'): - default_account = acc_template_ref.get(template.property_account_expense_categ.id) - elif journal_type == 'situation': - if type == 'debit': - default_account = acc_template_ref.get(template.property_account_expense_opening.id) + if tax_amount: + if tax.price_include: + total_excluded -= tax_amount + base -= tax_amount else: - default_account = acc_template_ref.get(template.property_account_income_opening.id) - return default_account + total_included += tax_amount - journal_names = { - 'sale': _('Sales Journal'), - 'purchase': _('Purchase Journal'), - 'sale_refund': _('Sales Refund Journal'), - 'purchase_refund': _('Purchase Refund Journal'), - 'general': _('Miscellaneous Journal'), - 'situation': _('Opening Entries Journal'), - } - journal_codes = { - 'sale': _('SAJ'), - 'purchase': _('EXJ'), - 'sale_refund': _('SCNJ'), - 'purchase_refund': _('ECNJ'), - 'general': _('MISC'), - 'situation': _('OPEJ'), - } + if tax.include_base_amount: + base += tax_amount - obj_data = self.pool.get('ir.model.data') - analytic_journal_obj = self.pool.get('account.analytic.journal') - template = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context) - - journal_data = [] - for journal_type in ['sale', 'purchase', 'sale_refund', 'purchase_refund', 'general', 'situation']: - vals = { - 'type': journal_type, - 'name': journal_names[journal_type], - 'code': journal_codes[journal_type], - 'company_id': company_id, - 'centralisation': journal_type == 'situation', - 'analytic_journal_id': _get_analytic_journal(journal_type), - 'default_credit_account_id': _get_default_account(journal_type, 'credit'), - 'default_debit_account_id': _get_default_account(journal_type, 'debit'), - } - journal_data.append(vals) - return journal_data - - def generate_properties(self, cr, uid, chart_template_id, acc_template_ref, company_id, context=None): - """ - This method used for creating properties. - - :param chart_template_id: id of the current chart template for which we need to create properties - :param acc_template_ref: Mapping between ids of account templates and real accounts created from them - :param company_id: company_id selected from wizard.multi.charts.accounts. - :returns: True - """ - property_obj = self.pool.get('ir.property') - field_obj = self.pool.get('ir.model.fields') - todo_list = [ - ('property_account_receivable','res.partner','account.account'), - ('property_account_payable','res.partner','account.account'), - ('property_account_expense_categ','product.category','account.account'), - ('property_account_income_categ','product.category','account.account'), - ('property_account_expense','product.template','account.account'), - ('property_account_income','product.template','account.account'), - ] - template = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context) - for record in todo_list: - account = getattr(template, record[0]) - value = account and 'account.account,' + str(acc_template_ref[account.id]) or False - if value: - field = field_obj.search(cr, uid, [('name', '=', record[0]),('model', '=', record[1]),('relation', '=', record[2])], context=context) - vals = { - 'name': record[0], - 'company_id': company_id, - 'fields_id': field[0], - 'value': value, - } - property_ids = property_obj.search(cr, uid, [('name','=', record[0]),('company_id', '=', company_id)], context=context) - if property_ids: - #the property exist: modify it - property_obj.write(cr, uid, property_ids, vals, context=context) - else: - #create the property - property_obj.create(cr, uid, vals, context=context) - return True - - def _install_template(self, cr, uid, template_id, company_id, code_digits=None, obj_wizard=None, acc_ref=None, taxes_ref=None, tax_code_ref=None, context=None): - ''' - This function recursively loads the template objects and create the real objects from them. - - :param template_id: id of the chart template to load - :param company_id: id of the company the wizard is running for - :param code_digits: integer that depicts the number of digits the accounts code should have in the COA - :param obj_wizard: the current wizard for generating the COA from the templates - :param acc_ref: Mapping between ids of account templates and real accounts created from them - :param taxes_ref: Mapping between ids of tax templates and real taxes created from them - :param tax_code_ref: Mapping between ids of tax code templates and real tax codes created from them - :returns: return a tuple with a dictionary containing - * the mapping between the account template ids and the ids of the real accounts that have been generated - from them, as first item, - * a similar dictionary for mapping the tax templates and taxes, as second item, - * a last identical containing the mapping of tax code templates and tax codes - :rtype: tuple(dict, dict, dict) - ''' - if acc_ref is None: - acc_ref = {} - if taxes_ref is None: - taxes_ref = {} - if tax_code_ref is None: - tax_code_ref = {} - template = self.pool.get('account.chart.template').browse(cr, uid, template_id, context=context) - if template.parent_id: - tmp1, tmp2, tmp3 = self._install_template(cr, uid, template.parent_id.id, company_id, code_digits=code_digits, acc_ref=acc_ref, taxes_ref=taxes_ref, tax_code_ref=tax_code_ref, context=context) - acc_ref.update(tmp1) - taxes_ref.update(tmp2) - tax_code_ref.update(tmp3) - tmp1, tmp2, tmp3 = self._load_template(cr, uid, template_id, company_id, code_digits=code_digits, obj_wizard=obj_wizard, account_ref=acc_ref, taxes_ref=taxes_ref, tax_code_ref=tax_code_ref, context=context) - acc_ref.update(tmp1) - taxes_ref.update(tmp2) - tax_code_ref.update(tmp3) - return acc_ref, taxes_ref, tax_code_ref - - def _load_template(self, cr, uid, template_id, company_id, code_digits=None, obj_wizard=None, account_ref=None, taxes_ref=None, tax_code_ref=None, context=None): - ''' - This function generates all the objects from the templates - - :param template_id: id of the chart template to load - :param company_id: id of the company the wizard is running for - :param code_digits: integer that depicts the number of digits the accounts code should have in the COA - :param obj_wizard: the current wizard for generating the COA from the templates - :param acc_ref: Mapping between ids of account templates and real accounts created from them - :param taxes_ref: Mapping between ids of tax templates and real taxes created from them - :param tax_code_ref: Mapping between ids of tax code templates and real tax codes created from them - :returns: return a tuple with a dictionary containing - * the mapping between the account template ids and the ids of the real accounts that have been generated - from them, as first item, - * a similar dictionary for mapping the tax templates and taxes, as second item, - * a last identical containing the mapping of tax code templates and tax codes - :rtype: tuple(dict, dict, dict) - ''' - if account_ref is None: - account_ref = {} - if taxes_ref is None: - taxes_ref = {} - if tax_code_ref is None: - tax_code_ref = {} - template = self.pool.get('account.chart.template').browse(cr, uid, template_id, context=context) - obj_tax_code_template = self.pool.get('account.tax.code.template') - obj_acc_tax = self.pool.get('account.tax') - obj_tax_temp = self.pool.get('account.tax.template') - obj_acc_template = self.pool.get('account.account.template') - obj_fiscal_position_template = self.pool.get('account.fiscal.position.template') - - # create all the tax code. - tax_code_ref.update(obj_tax_code_template.generate_tax_code(cr, uid, template.tax_code_root_id.id, company_id, context=context)) - - # Generate taxes from templates. - tax_templates = [x for x in template.tax_template_ids] - generated_tax_res = obj_tax_temp._generate_tax(cr, uid, tax_templates, tax_code_ref, company_id, context=context) - taxes_ref.update(generated_tax_res['tax_template_to_tax']) - - # Generating Accounts from templates. - account_template_ref = obj_acc_template.generate_account(cr, uid, template_id, taxes_ref, account_ref, code_digits, company_id, context=context) - account_ref.update(account_template_ref) - - # writing account values on tax after creation of accounts - for key,value in generated_tax_res['account_dict'].items(): - if value['account_collected_id'] or value['account_paid_id']: - obj_acc_tax.write(cr, uid, [key], { - 'account_collected_id': account_ref.get(value['account_collected_id'], False), - 'account_paid_id': account_ref.get(value['account_paid_id'], False), + taxes.append({ + 'id': tax.id, + 'name': tax.name, + 'amount': tax_amount, + 'sequence': tax.sequence, + 'account_id': tax.account_id.id, + 'refund_account_id': tax.refund_account_id.id, + 'analytic': tax.analytic, }) - # Create Journals - self.generate_journals(cr, uid, template_id, account_ref, company_id, context=context) - - # generate properties function - self.generate_properties(cr, uid, template_id, account_ref, company_id, context=context) - - # Generate Fiscal Position , Fiscal Position Accounts and Fiscal Position Taxes from templates - obj_fiscal_position_template.generate_fiscal_position(cr, uid, template_id, taxes_ref, account_ref, company_id, context=context) - - return account_ref, taxes_ref, tax_code_ref - - def _create_tax_templates_from_rates(self, cr, uid, obj_wizard, company_id, context=None): - ''' - This function checks if the chosen chart template is configured as containing a full set of taxes, and if - it's not the case, it creates the templates for account.tax.code and for account.account.tax objects accordingly - to the provided sale/purchase rates. Then it saves the new tax templates as default taxes to use for this chart - template. - - :param obj_wizard: browse record of wizard to generate COA from templates - :param company_id: id of the company for wich the wizard is running - :return: True - ''' - obj_tax_code_template = self.pool.get('account.tax.code.template') - obj_tax_temp = self.pool.get('account.tax.template') - chart_template = obj_wizard.chart_template_id - vals = {} - all_parents = self._get_chart_parent_ids(cr, uid, chart_template, context=context) - # create tax templates and tax code templates from purchase_tax_rate and sale_tax_rate fields - if not chart_template.complete_tax_set: - value = obj_wizard.sale_tax_rate - ref_tax_ids = obj_tax_temp.search(cr, uid, [('type_tax_use','in', ('sale','all')), ('chart_template_id', 'in', all_parents)], context=context, order="sequence, id desc", limit=1) - obj_tax_temp.write(cr, uid, ref_tax_ids, {'amount': value/100.0, 'name': _('Tax %.2f%%') % value}) - value = obj_wizard.purchase_tax_rate - ref_tax_ids = obj_tax_temp.search(cr, uid, [('type_tax_use','in', ('purchase','all')), ('chart_template_id', 'in', all_parents)], context=context, order="sequence, id desc", limit=1) - obj_tax_temp.write(cr, uid, ref_tax_ids, {'amount': value/100.0, 'name': _('Purchase Tax %.2f%%') % value}) - return True - - def execute(self, cr, uid, ids, context=None): - ''' - This function is called at the confirmation of the wizard to generate the COA from the templates. It will read - all the provided information to create the accounts, the banks, the journals, the taxes, the tax codes, the - accounting properties... accordingly for the chosen company. - ''' - if uid != SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'): - raise openerp.exceptions.AccessError(_("Only administrators can change the settings")) - obj_data = self.pool.get('ir.model.data') - ir_values_obj = self.pool.get('ir.values') - obj_wizard = self.browse(cr, uid, ids[0]) - company_id = obj_wizard.company_id.id - - self.pool.get('res.company').write(cr, uid, [company_id], {'currency_id': obj_wizard.currency_id.id, 'accounts_code_digits': obj_wizard.code_digits}, context=context) - - # When we install the CoA of first company, set the currency to price types and pricelists - if company_id==1: - for ref in (('product','list_price'),('product','standard_price'),('product','list0'),('purchase','list0')): - try: - tmp2 = obj_data.get_object_reference(cr, uid, *ref) - if tmp2: - self.pool[tmp2[0]].write(cr, uid, tmp2[1], { - 'currency_id': obj_wizard.currency_id.id - }) - except ValueError: - pass - - # If the floats for sale/purchase rates have been filled, create templates from them - self._create_tax_templates_from_rates(cr, uid, obj_wizard, company_id, context=context) - - # Install all the templates objects and generate the real objects - acc_template_ref, taxes_ref, tax_code_ref = self._install_template(cr, uid, obj_wizard.chart_template_id.id, company_id, code_digits=obj_wizard.code_digits, obj_wizard=obj_wizard, context=context) - - # write values of default taxes for product as super user - if obj_wizard.sale_tax and taxes_ref: - ir_values_obj.set_default(cr, SUPERUSER_ID, 'product.template', "taxes_id", [taxes_ref[obj_wizard.sale_tax.id]], for_all_users=True, company_id=company_id) - if obj_wizard.purchase_tax and taxes_ref: - ir_values_obj.set_default(cr, SUPERUSER_ID, 'product.template', "supplier_taxes_id", [taxes_ref[obj_wizard.purchase_tax.id]], for_all_users=True, company_id=company_id) - - # Create Bank journals - self._create_bank_journals_from_o2m(cr, uid, obj_wizard, company_id, acc_template_ref, context=context) - return {} - - def _prepare_bank_journal(self, cr, uid, company, line, default_account_id, context=None): - ''' - This function prepares the value to use for the creation of a bank journal created through the wizard of - generating COA from templates. - - :param line: dictionary containing the values encoded by the user related to his bank account - :param default_account_id: id of the default debit.credit account created before for this journal. - :param company_id: id of the company for which the wizard is running - :return: mapping of field names and values - :rtype: dict - ''' - obj_journal = self.pool.get('account.journal') - - # we need to loop to find next number for journal code - for num in xrange(1, 100): - # journal_code has a maximal size of 5, hence we can enforce the boundary num < 100 - journal_code = _('BNK')[:3] + str(num) - ids = obj_journal.search(cr, uid, [('code', '=', journal_code), ('company_id', '=', company.id)], context=context) - if not ids: - break - else: - raise UserError(_('Cannot generate an unused journal code.')) - return { - 'name': line['acc_name'], - 'code': journal_code, - 'type': line['account_type'] == 'cash' and 'cash' or 'bank', - 'company_id': company.id, - 'analytic_journal_id': False, - 'currency': line['currency_id'] or False, - 'default_credit_account_id': default_account_id, - 'default_debit_account_id': default_account_id, - } - - def _prepare_bank_account(self, cr, uid, company, line, acc_template_ref=False, ref_acc_bank=False, context=None): - ''' - This function prepares the value to use for the creation of the default debit and credit accounts of a - bank journal created through the wizard of generating COA from templates. - - :param company: company for which the wizard is running - :param line: dictionary containing the values encoded by the user related to his bank account - :param acc_template_ref: the dictionary containing the mapping between the ids of account templates and the ids - of the accounts that have been generated from them. - :param ref_acc_bank: browse record of the account template set as root of all bank accounts for the chosen - template - :return: mapping of field names and values - :rtype: dict - ''' - obj_data = self.pool.get('ir.model.data') - obj_acc = self.pool.get('account.account') - - # Seek the next available number for the account code - code_digits = company.accounts_code_digits or 0 - bank_account_code_char = company.bank_account_code_char or '' - for num in xrange(1, 100): - new_code = str(bank_account_code_char.ljust(code_digits - 1, '0')) + str(num) - ids = obj_acc.search(cr, uid, [('code', '=', new_code), ('company_id', '=', company.id)]) - if not ids: - break - else: - raise UserError(_('Cannot generate an unused account code.')) - - # Get the id of the user types fr-or cash and bank - tmp = obj_data.get_object_reference(cr, uid, 'account', 'data_account_type_cash') - cash_type = tmp and tmp[1] or False - tmp = obj_data.get_object_reference(cr, uid, 'account', 'data_account_type_bank') - bank_type = tmp and tmp[1] or False - parent_id = False - if acc_template_ref: - parent_id = acc_template_ref[ref_acc_bank.id] - else: - tmp = self.pool.get('account.account').search(cr, uid, [('code', '=', company.bank_account_code_char)], context=context) - if tmp: - parent_id = tmp[0] - - return { - 'name': line['acc_name'], - 'currency_id': line['currency_id'] or False, - 'code': new_code, - 'type': 'liquidity', - 'user_type': line['account_type'] == 'cash' and cash_type or bank_type, - # TODO: refactor when parent_id removed from account.account - 'parent_id': parent_id, - 'company_id': company.id, + 'taxes': taxes, + 'total_excluded': currency.round(total_excluded), + 'total_included': currency.round(total_included), } - def _create_bank_journals_from_o2m(self, cr, uid, obj_wizard, company_id, acc_template_ref, context=None): - ''' - This function creates bank journals and its accounts for each line encoded in the field bank_accounts_id of the - wizard. - - :param obj_wizard: the current wizard that generates the COA from the templates. - :param company_id: the id of the company for which the wizard is running. - :param acc_template_ref: the dictionary containing the mapping between the ids of account templates and the ids - of the accounts that have been generated from them. - :return: True - ''' - obj_acc = self.pool.get('account.account') - obj_journal = self.pool.get('account.journal') - company = self.pool.get('res.company').browse(cr, uid, company_id, context=context) - - # Build a list with all the data to process - journal_data = [] - if obj_wizard.bank_accounts_id: - for acc in obj_wizard.bank_accounts_id: - vals = { - 'acc_name': acc.acc_name, - 'account_type': acc.account_type, - 'currency_id': acc.currency_id.id, - } - journal_data.append(vals) - - ref_acc_bank = obj_wizard.chart_template_id.bank_account_view_id - if journal_data and not ref_acc_bank.code: - raise UserError(_('You have to set a code for the bank account defined on the selected chart of accounts.')) - self.pool.get('res.company').write(cr, uid, [company.id], {'bank_account_code_char': ref_acc_bank.code}, context=context) - - for line in journal_data: - # Create the default debit/credit accounts for this bank journal - vals = self._prepare_bank_account(cr, uid, company, line, acc_template_ref, ref_acc_bank, context=context) - default_account_id = obj_acc.create(cr, uid, vals, context=context) - - #create the bank journal - vals_journal = self._prepare_bank_journal(cr, uid, company, line, default_account_id, context=context) - obj_journal.create(cr, uid, vals_journal) - return True + @api.v7 + def compute_all(self, cr, uid, ids, price_unit, currency_id=None, quantity=1.0, product_id=None, partner_id=None, context=None): + currency = currency_id and self.pool.get('res.currency').browse(cr, uid, currency_id, context=context) or None + product = product_id and self.pool.get('product.product').browse(cr, uid, product_id, context=context) or None + partner = partner_id and self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) or None + ids = isinstance(ids, (int, long)) and [ids] or ids + recs = self.browse(cr, uid, ids, context=context) + return recs.compute_all(price_unit, currency, quantity, product, partner) -class account_bank_accounts_wizard(osv.osv_memory): - _name='account.bank.accounts.wizard' +class AccountOperationTemplate(models.Model): + _name = "account.operation.template" + _description = "Preset to create journal entries during a invoices and payments matching" - _columns = { - 'acc_name': fields.char('Account Name.', required=True), - 'bank_account_id': fields.many2one('wizard.multi.charts.accounts', 'Bank Account', required=True, ondelete='cascade'), - 'currency_id': fields.many2one('res.currency', 'Secondary Currency', help="Forces all moves for this account to have this secondary currency."), - 'account_type': fields.selection([('cash','Cash'), ('check','Check'), ('bank','Bank')], 'Account Type'), - } + name = fields.Char(string='Button Label', required=True) + sequence = fields.Integer(required=True, default=10) + has_second_line = fields.Boolean(string='Second line', default=False) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) + + account_id = fields.Many2one('account.account', string='Account', ondelete='cascade', domain=[('deprecated', '=', False)]) + journal_id = fields.Many2one('account.journal', string='Journal', ondelete='cascade', help="This field is ignored in a bank statement reconciliation.") + label = fields.Char(string='Journal Item Label') + amount_type = fields.Selection([ + ('fixed', 'Fixed'), + ('percentage', 'Percentage of amount') + ], required=True, default='percentage') + amount = fields.Float(digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") + tax_id = fields.Many2one('account.tax', string='Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) + analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null', domain=[('state', 'not in', ('close', 'cancelled'))]) + + second_account_id = fields.Many2one('account.account', string='Account', ondelete='cascade', domain=[('deprecated', '=', False)]) + second_journal_id = fields.Many2one('account.journal', string='Journal', ondelete='cascade', help="This field is ignored in a bank statement reconciliation.") + second_label = fields.Char(string='Journal Item Label') + second_amount_type = fields.Selection([ + ('fixed', 'Fixed'), + ('percentage', 'Percentage of amount') + ], string='Amount type', required=True, default='percentage') + second_amount = fields.Float(string='Amount', digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") + second_tax_id = fields.Many2one('account.tax', string='Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) + second_analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null', domain=[('state', 'not in', ('close', 'cancelled'))]) + + @api.onchange('name') + def onchange_name(self): + self.label = self.name \ No newline at end of file diff --git a/addons/account/account_analytic_line.py b/addons/account/account_analytic_line.py index 81d10e66f4a..bbc04f454d1 100644 --- a/addons/account/account_analytic_line.py +++ b/addons/account/account_analytic_line.py @@ -1,80 +1,39 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 openerp.osv import fields -from openerp.osv import osv -from openerp.tools.translate import _ +from openerp import api, fields, models, _ from openerp.exceptions import UserError -class account_analytic_line(osv.osv): + +class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' _description = 'Analytic Line' - _columns = { - 'product_uom_id': fields.many2one('product.uom', 'Unit of Measure'), - 'product_id': fields.many2one('product.product', 'Product'), - 'general_account_id': fields.many2one('account.account', 'Financial Account', required=True, ondelete='restrict'), - 'move_id': fields.many2one('account.move.line', 'Move Line', ondelete='cascade', select=True), - 'journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal', required=True, ondelete='restrict', select=True), - 'code': fields.char('Code', size=8), - 'ref': fields.char('Ref.'), - 'currency_id': fields.related('move_id', 'currency_id', type='many2one', relation='res.currency', string='Account Currency', store=True, help="The related account currency if not equal to the company one.", readonly=True), - 'amount_currency': fields.related('move_id', 'amount_currency', type='float', string='Amount Currency', store=True, help="The amount expressed in the related account currency if not equal to the company one.", readonly=True), - 'partner_id': fields.related('account_id', 'partner_id', type='many2one', relation='res.partner', string='Partner', store=True), - } - - _defaults = { - 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c), - } _order = 'date desc' - def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if context is None: - context = {} - if context.get('from_date',False): - args.append(['date', '>=', context['from_date']]) - if context.get('to_date',False): - args.append(['date','<=', context['to_date']]) - return super(account_analytic_line, self).search(cr, uid, args, offset, limit, - order, context=context, count=count) - - def _check_company(self, cr, uid, ids, context=None): - lines = self.browse(cr, uid, ids, context=context) - for l in lines: - if l.move_id and not l.account_id.company_id.id == l.move_id.account_id.company_id.id: - return False - return True + product_uom_id = fields.Many2one('product.uom', string='Unit of Measure') + product_id = fields.Many2one('product.product', string='Product') + general_account_id = fields.Many2one('account.account', string='Financial Account', required=True, ondelete='restrict', domain=[('deprecated', '=', False)]) + move_id = fields.Many2one('account.move.line', string='Move Line', ondelete='cascade', index=True) + journal_id = fields.Many2one('account.analytic.journal', string='Analytic Journal', required=True, ondelete='restrict', index=True) + code = fields.Char(size=8) + ref = fields.Char(string='Ref.') + currency_id = fields.Many2one('res.currency', related='move_id.currency_id', string='Account Currency', store=True, help="The related account currency if not equal to the company one.", readonly=True) + amount_currency = fields.Monetary(related='move_id.amount_currency', store=True, help="The amount expressed in the related account currency if not equal to the company one.", readonly=True) + partner_id = fields.Many2one('res.partner', related='account_id.partner_id', string='Partner', store=True) # Compute the cost based on the price type define into company # property_valuation_price_type property + @api.v7 def on_change_unit_amount(self, cr, uid, id, prod_id, quantity, company_id, unit=False, journal_id=False, context=None): - if context==None: - context={} + if context is None: + context = {} if not journal_id: - j_ids = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=','purchase')]) + j_ids = self.pool.get('account.analytic.journal').search(cr, uid, [('type', '=', 'purchase')]) journal_id = j_ids and j_ids[0] or False if not journal_id or not prod_id: return {} product_obj = self.pool.get('product.product') - analytic_journal_obj =self.pool.get('account.analytic.journal') + analytic_journal_obj = self.pool.get('account.analytic.journal') product_price_type_obj = self.pool.get('product.price.type') product_uom_obj = self.pool.get('product.uom') j_id = analytic_journal_obj.browse(cr, uid, journal_id, context=context) @@ -94,8 +53,7 @@ class account_analytic_line(osv.osv): if not a: a = prod.categ_id.property_account_expense_categ.id if not a: - raise UserError( - _('There is no expense account defined ' \ + raise UserError(_('There is no expense account defined ' \ 'for this product: "%s" (id:%d).') % \ (prod.name, prod.id,)) else: @@ -103,19 +61,18 @@ class account_analytic_line(osv.osv): if not a: a = prod.categ_id.property_account_income_categ.id if not a: - raise UserError( - _('There is no income account defined ' \ + raise UserError(_('There is no income account defined ' \ 'for this product: "%s" (id:%d).') % \ (prod.name, prod_id,)) flag = False # Compute based on pricetype - product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','standard_price')], context=context) + product_price_type_ids = product_price_type_obj.search(cr, uid, [('field', '=', 'standard_price')], context=context) pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0] if journal_id: journal = analytic_journal_obj.browse(cr, uid, journal_id, context=context) if journal.type == 'sale': - product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','list_price')], context=context) + product_price_type_ids = product_price_type_obj.search(cr, uid, [('field', '=', 'list_price')], context=context) if product_price_type_ids: pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0] # Take the company currency as the reference one @@ -127,9 +84,10 @@ class account_analytic_line(osv.osv): # to return a default price for those units ctx['uom'] = unit amount_unit = prod.price_get(pricetype.field, context=ctx)[prod.id] - prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account') amount = amount_unit * quantity or 0.0 - result = round(amount, prec) + cur_record = self.browse(cr, uid, id, context=context) + currency = cur_record.exists() and cur_record.currency_id or prod.company_id.currency_id + result = round(amount, currency.decimal_places) if not flag: result *= -1 return {'value': { @@ -139,24 +97,63 @@ class account_analytic_line(osv.osv): } } - def view_header_get(self, cr, user, view_id, view_type, context=None): - if context is None: - context = {} + @api.v8 + @api.onchange('product_id', 'product_uom_id') + def on_change_unit_amount(self): + product_price_type_obj = self.env['product.price.type'] + + journal_id = self.journal_id + if not journal_id: + journal_id = self.env['account.analytic.journal'].search([('type', '=', 'purchase')], limit=1) + if not journal_id or not self.product_id: + return {} + + result = 0.0 + if self.product_id: + unit = self.product_uom_id.id + if not self.product_uom_id or self.product_id.uom_id.category_id.id != self.product_uom_id.category_id.id: + unit = self.product_id.uom_id.id + if journal_id.type == 'purchase': + if not self.product_uom_id or self.product_id.uom_po_id.category_id.id != self.product_uom_id.category_id.id: + unit = self.product_id.uom_po_id.id + if journal_id.type != 'sale': + account = self.product_id.property_account_expense.id or self.product_id.categ_id.property_account_expense_categ.id + if not account: + raise UserError(_('There is no expense account defined ' \ + 'for this product: "%s" (id:%d).') % \ + (self.product_id.name, self.product_id.id,)) + else: + account = self.product_id.property_account_income.id or self.product_id.categ_id.property_account_income_categ.id + if not account: + raise UserError(_('There is no income account defined ' \ + 'for this product: "%s" (id:%d).') % \ + (self.product_id.name, self.product_id.id,)) + + # Compute based on pricetype + if journal_id.type == 'sale': + pricetype = product_price_type_obj.search([('field', '=', 'list_price')], limit=1) + else: + pricetype = product_price_type_obj.search([('field', '=', 'standard_price')], limit=1) + + ctx = dict(self._context or {}) + if unit: + # price_get() will respect a 'uom' in its context, in order + # to return a default price for those units + ctx['uom'] = unit + amount_unit = self.product_id.with_context(ctx).price_get(pricetype.field)[self.product_id.id] + amount = amount_unit * self.unit_amount or 0.0 + result = round(amount, self.currency_id.decimal_places) + if pricetype.field != 'list_price': + result *= -1 + self.amount = result + self.general_account_id = account + self.product_uom_id = unit + + @api.model + def view_header_get(self, view_id, view_type): + context = (self._context or {}) + header = False if context.get('account_id', False): - # account_id in context may also be pointing to an account.account.id - cr.execute('select name from account_analytic_account where id=%s', (context['account_id'],)) - res = cr.fetchone() - if res: - res = _('Entries: ')+ (res[0] or '') - return res - return False - - -class res_partner(osv.osv): - """ Inherits partner and adds contract information in the partner form """ - _inherit = 'res.partner' - - _columns = { - 'contract_ids': fields.one2many('account.analytic.account', \ - 'partner_id', 'Contracts', readonly=True), - } + analytic_account = self.env['account.analytic.account'].search([('id', '=', context['account_id'])], limit=1) + header = _('Entries: ') + (analytic_account.name or '') + return header \ No newline at end of file diff --git a/addons/account/account_bank.py b/addons/account/account_bank.py index 27fc338072e..e93847f4465 100644 --- a/addons/account/account_bank.py +++ b/addons/account/account_bank.py @@ -1,99 +1,82 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 openerp.tools.translate import _ -from openerp.osv import fields, osv +from openerp import api, fields, models, _ -class bank(osv.osv): + +class Bank(models.Model): _inherit = "res.partner.bank" - _columns = { - 'journal_id': fields.many2one('account.journal', 'Account Journal', help="This journal will be created automatically for this bank account when you save the record"), - 'currency_id': fields.related('journal_id', 'currency', type="many2one", relation='res.currency', readonly=True, - string="Currency", help="Currency of the related account journal."), - } - def create(self, cr, uid, data, context=None): - result = super(bank, self).create(cr, uid, data, context=context) - self.post_write(cr, uid, [result], context=context) + + journal_id = fields.Many2one('account.journal', string='Account Journal', + help="This journal will be created automatically for this bank account when you save the record") + currency_id = fields.Many2one('res.currency', related='journal_id.currency_id', string='Currency', + readonly=True, help="Currency of the related account journal.") + + @api.model + def create(self, data): + result = super(Bank, self).create(data) + result.post_write() return result - def write(self, cr, uid, ids, data, context=None): - result = super(bank, self).write(cr, uid, ids, data, context=context) - self.post_write(cr, uid, ids, context=context) + @api.multi + def write(self, data): + result = super(Bank, self).write(data) + self.post_write() return result + @api.model def _prepare_name(self, bank): "Return the name to use when creating a bank journal" return (bank.bank_name or '') + ' ' + (bank.acc_number or '') - def _prepare_name_get(self, cr, uid, bank_dicts, context=None): + @api.model + def _prepare_name_get(self, bank_dicts): """Add ability to have %(currency_name)s in the format_layout of res.partner.bank.type""" currency_ids = list(set(data['currency_id'][0] for data in bank_dicts if data.get('currency_id'))) - currencies = self.pool.get('res.currency').browse(cr, uid, currency_ids, context=context) + currencies = self.env['res.currency'].browse(currency_ids) currency_name = dict((currency.id, currency.name) for currency in currencies) for data in bank_dicts: data['currency_name'] = data.get('currency_id') and currency_name[data['currency_id'][0]] or '' - return super(bank, self)._prepare_name_get(cr, uid, bank_dicts, context=context) + return super(Bank, self)._prepare_name_get(bank_dicts) - def post_write(self, cr, uid, ids, context=None): - if isinstance(ids, (int, long)): - ids = [ids] + @api.multi + def post_write(self): + AccountObj = self.env['account.account'] + JournalObj = self.env['account.journal'] - obj_acc = self.pool.get('account.account') - obj_data = self.pool.get('ir.model.data') - - for bank in self.browse(cr, uid, ids, context): - if bank.company_id and not bank.journal_id: + for bank in self: + # Create a journal for the bank account if it belongs to the company. + if bank.partner_id in self.env.user.company_ids.mapped('partner_id') and not bank.journal_id: + # TODO: this code is duplicated; refactor using _prepare_bank_account() in chart_template.py # Find the code and parent of the bank account to create dig = 6 current_num = 1 - ids = obj_acc.search(cr, uid, [('type','=','liquidity'), ('company_id', '=', bank.company_id.id), ('parent_id', '!=', False)], context=context) + account = AccountObj.search([('internal_type', '=', 'liquidity'), ('company_id', '=', bank.company_id.id)], limit=1) # No liquidity account exists, no template available - if not ids: continue + if not account: continue - ref_acc_bank = obj_acc.browse(cr, uid, ids[0], context=context).parent_id + ref_acc_bank = account while True: new_code = str(ref_acc_bank.code.ljust(dig-len(str(current_num)), '0')) + str(current_num) - ids = obj_acc.search(cr, uid, [('code', '=', new_code), ('company_id', '=', bank.company_id.id)]) - if not ids: + account = AccountObj.search([('code', '=', new_code), ('company_id', '=', bank.company_id.id)], limit=1) + if not account: break current_num += 1 name = self._prepare_name(bank) acc = { 'name': name, 'code': new_code, - 'type': 'liquidity', 'user_type': ref_acc_bank.user_type.id, 'reconcile': False, - 'parent_id': ref_acc_bank.id, 'company_id': bank.company_id.id, } - acc_bank_id = obj_acc.create(cr,uid,acc,context=context) + acc_bank = AccountObj.create(acc) - jour_obj = self.pool.get('account.journal') new_code = 1 while True: code = _('BNK')+str(new_code) - ids = jour_obj.search(cr, uid, [('code','=',code)], context=context) - if not ids: + account = JournalObj.search([('code', '=', code)], limit=1) + if not account: break new_code += 1 @@ -104,10 +87,9 @@ class bank(osv.osv): 'type': 'bank', 'company_id': bank.company_id.id, 'analytic_journal_id': False, - 'default_credit_account_id': acc_bank_id, - 'default_debit_account_id': acc_bank_id, + 'default_credit_account_id': acc_bank.id, + 'default_debit_account_id': acc_bank.id, } - journal_id = jour_obj.create(cr, uid, vals_journal, context=context) - self.write(cr, uid, [bank.id], {'journal_id': journal_id}, context=context) + bank.journal_id = JournalObj.create(vals_journal) return True diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 8493bcebe24..3d55ab7ac65 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -1,936 +1,844 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 openerp.osv import fields, osv, expression -from openerp.tools.translate import _ -import openerp.addons.decimal_precision as dp -from openerp.report import report_sxw +from openerp import api, fields, models, _ +from openerp.osv import expression from openerp.tools import float_compare, float_round -from openerp.exceptions import UserError, RedirectWarning +from openerp.tools.misc import formatLang +from openerp.exceptions import UserError, ValidationError -from operator import add import time -class account_bank_statement(osv.osv): - def create(self, cr, uid, vals, context=None): - if vals.get('name', '/') == '/': - journal_id = vals.get('journal_id', self._default_journal_id(cr, uid, context=context)) - vals['name'] = self._compute_default_statement_name(cr, uid, journal_id, context=context) - if vals.get('line_ids'): - for idx, line in enumerate(vals['line_ids']): - line[2]['sequence'] = idx + 1 - return super(account_bank_statement, self).create(cr, uid, vals, context=context) +class AccountCashboxLine(models.Model): + """ Cash Box Details """ + _name = 'account.cashbox.line' + _description = 'CashBox Line' + _rec_name = 'coin_value' + _order = 'coin_value' - def write(self, cr, uid, ids, vals, context=None): - res = super(account_bank_statement, self).write(cr, uid, ids, vals, context=context) - account_bank_statement_line_obj = self.pool.get('account.bank.statement.line') - for statement in self.browse(cr, uid, ids, context): - for idx, line in enumerate(statement.line_ids): - account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': idx + 1}, context=context) - return res + @api.one + @api.depends('coin_value', 'number') + def _sub_total(self): + """ Calculates Sub total""" + self.subtotal = self.coin_value * self.number - def _default_journal_id(self, cr, uid, context=None): - if context is None: - context = {} - journal_pool = self.pool.get('account.journal') - journal_type = context.get('journal_type', False) - company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=context) + coin_value = fields.Float(string='Unit of Currency', required=True, digits=0) + number = fields.Integer(string='Opening Number of Units', help='Opening Unit Numbers') + subtotal = fields.Float(compute='_sub_total', string='Subtotal', digits=0, readonly=True) + cashbox_id = fields.Many2one('account.bank.statement.cashbox') + + +class AccountBankStmtCashWizard(models.Model): + """ + Account Bank Statement popup that allows entering cash details. + """ + _name = 'account.bank.statement.cashbox' + _description = 'Account Bank Statement Cashbox Details' + + cashbox_lines_ids = fields.One2many('account.cashbox.line', 'cashbox_id', string='Cashbox Lines') + + @api.multi + def validate(self): + bnk_stmt_id = self.env.context.get('active_id', False) + bnk_stmt = self.env['account.bank.statement'].browse(bnk_stmt_id) + total = 0.0 + for lines in self.cashbox_lines_ids: + total += lines.subtotal + if self.env.context.get('balance', False) == 'start': + #starting balance + bnk_stmt.write({'balance_start': total, 'cashbox_start_id': self.id}) + else: + #closing balance + bnk_stmt.write({'balance_end_real': total, 'cashbox_end_id': self.id}) + return {'type': 'ir.actions.act_window_close'} + + +class AccountBankStmtCloseCheck(models.TransientModel): + """ + Account Bank Statement wizard that check that closing balance is correct. + """ + _name = 'account.bank.statement.closebalance' + _description = 'Account Bank Statement closing balance' + + @api.multi + def validate(self): + bnk_stmt_id = self.env.context.get('active_id', False) + if bnk_stmt_id: + self.env['account.bank.statement'].browse(bnk_stmt_id).button_confirm_bank() + return {'type': 'ir.actions.act_window_close'} + + +class AccountBankStatement(models.Model): + + @api.one + @api.depends('line_ids', 'balance_start', 'line_ids.amount', 'balance_end_real') + def _end_balance(self): + self.total_entry_encoding = sum([line.amount for line in self.line_ids]) + self.balance_end = self.balance_start + self.total_entry_encoding + self.difference = self.balance_end_real - self.balance_end + + @api.one + @api.depends('journal_id') + def _compute_currency(self): + self.currency_id = self.journal_id.currency_id or self.env.user.company_id.currency_id + + @api.one + @api.depends('line_ids.journal_entry_ids') + def _check_lines_reconciled(self): + self.all_lines_reconciled = all([line.journal_entry_ids.ids or line.account_id.id for line in self.line_ids]) + + @api.model + def _default_journal(self): + journal_type = self.env.context.get('journal_type', False) + company_id = self.env['res.company']._company_default_get('account.bank.statement') if journal_type: - ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)]) - if ids: - return ids[0] + journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)]) + if journals: + return journals[0] return False - def _end_balance(self, cursor, user, ids, name, attr, context=None): - res = {} - for statement in self.browse(cursor, user, ids, context=context): - res[statement.id] = statement.balance_start - for line in statement.line_ids: - res[statement.id] += line.amount - return res - - def _get_period(self, cr, uid, context=None): - periods = self.pool.get('account.period').find(cr, uid, context=context) - if periods: - return periods[0] - return False - - def _compute_default_statement_name(self, cr, uid, journal_id, context=None): - context = dict(context or {}) - obj_seq = self.pool.get('ir.sequence') - period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context) - context['ir_sequence_date'] = period.date_start - journal = self.pool.get('account.journal').browse(cr, uid, journal_id, None) - return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context) - - def _currency(self, cursor, user, ids, name, args, context=None): - res = {} - res_currency_obj = self.pool.get('res.currency') - res_users_obj = self.pool.get('res.users') - default_currency = res_users_obj.browse(cursor, user, - user, context=context).company_id.currency_id - for statement in self.browse(cursor, user, ids, context=context): - currency = statement.journal_id.currency - if not currency: - currency = default_currency - res[statement.id] = currency.id - currency_names = {} - for currency_id, currency_name in res_currency_obj.name_get(cursor, - user, [x for x in res.values()], context=context): - currency_names[currency_id] = currency_name - for statement_id in res.keys(): - currency_id = res[statement_id] - res[statement_id] = (currency_id, currency_names[currency_id]) - return res - - def _get_statement(self, cr, uid, ids, context=None): - result = {} - for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context): - result[line.statement_id.id] = True - return result.keys() - - def _all_lines_reconciled(self, cr, uid, ids, name, args, context=None): - res = {} - for statement in self.browse(cr, uid, ids, context=context): - res[statement.id] = all([line.journal_entry_ids.ids for line in statement.line_ids]) - return res - - _order = "date desc, id desc" _name = "account.bank.statement" _description = "Bank Statement" + _order = "date desc, id desc" _inherit = ['mail.thread'] - _columns = { - 'name': fields.char( - 'Reference', states={'draft': [('readonly', False)]}, - readonly=True, # readonly for account_cash_statement - copy=False, - help='if you give the Name other then /, its created Accounting Entries Move ' - 'will be with same name as statement name. ' - 'This allows the statement entries to have the same references than the ' - 'statement itself'), - 'date': fields.date('Date', required=True, states={'confirm': [('readonly', True)]}, - select=True, copy=False), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True, - readonly=True, states={'draft':[('readonly',False)]}), - 'period_id': fields.many2one('account.period', 'Period', required=True, - states={'confirm':[('readonly', True)]}), - 'balance_start': fields.float('Starting Balance', digits_compute=dp.get_precision('Account'), - states={'confirm':[('readonly',True)]}), - 'balance_end_real': fields.float('Ending Balance', digits_compute=dp.get_precision('Account'), - states={'confirm': [('readonly', True)]}, help="Computed using the cash control lines"), - 'balance_end': fields.function(_end_balance, - store = { - 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids','balance_start'], 10), - 'account.bank.statement.line': (_get_statement, ['amount'], 10), - }, - string="Computed Balance", help='Balance as calculated based on Opening Balance and transaction lines'), - 'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), - 'line_ids': fields.one2many('account.bank.statement.line', - 'statement_id', 'Statement lines', - states={'confirm':[('readonly', True)]}, copy=True), - 'move_line_ids': fields.one2many('account.move.line', 'statement_id', - 'Entry lines', states={'confirm':[('readonly',True)]}), - 'state': fields.selection([('draft', 'New'), - ('open','Open'), # used by cash statements - ('confirm', 'Closed')], - 'Status', required=True, readonly="1", - copy=False, - help='When new statement is created the status will be \'Draft\'.\n' - 'And after getting confirmation from the bank it will be in \'Confirmed\' status.'), - 'currency': fields.function(_currency, string='Currency', - type='many2one', relation='res.currency'), - 'account_id': fields.related('journal_id', 'default_debit_account_id', type='many2one', relation='account.account', string='Account used in this journal', readonly=True, help='used in statement reconciliation domain, but shouldn\'t be used elswhere.'), - 'cash_control': fields.related('journal_id', 'cash_control' , type='boolean', relation='account.journal',string='Cash control'), - 'all_lines_reconciled': fields.function(_all_lines_reconciled, string='All lines reconciled', type='boolean'), - } - _defaults = { - 'name': '/', - 'date': fields.date.context_today, - 'state': 'draft', - 'journal_id': _default_journal_id, - 'period_id': _get_period, - 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=c), - } + name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True) + date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, select=True, copy=False, default=fields.Date.context_today) + date_done = fields.Datetime(string="Closed On") + balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}) + balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]}) + state = fields.Selection([('open', 'New'), ('confirm', 'Validated')], string='Status', required=True, readonly=True, copy=False, default='open') + currency_id = fields.Many2one('res.currency', compute='_compute_currency', oldname='currency') + journal_id = fields.Many2one('account.journal', string='Journal', required=True, states={'confirm': [('readonly', True)]}, default=_default_journal) + journal_type = fields.Selection(related='journal_id.type', help="Technical field used for usability purposes") + company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, + default=lambda self: self.env['res.company']._company_default_get('account.bank.statement')) - def _check_company_id(self, cr, uid, ids, context=None): - for statement in self.browse(cr, uid, ids, context=context): - if statement.company_id.id != statement.period_id.company_id.id: - return False - return True + total_entry_encoding = fields.Monetary('Transactions Subtotal', compute='_end_balance', store=True, help="Total of transaction lines.") + balance_end = fields.Monetary('Computed Balance', compute='_end_balance', store=True, help='Balance as calculated based on Opening Balance and transaction lines') + difference = fields.Monetary(compute='_end_balance', store=True, help="Difference between the computed ending balance and the specified ending balance.") - _constraints = [ - (_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']), - ] + line_ids = fields.One2many('account.bank.statement.line', 'statement_id', string='Statement lines', states={'confirm': [('readonly', True)]}, copy=True) + move_line_ids = fields.One2many('account.move.line', 'statement_id', string='Entry lines', states={'confirm': [('readonly', True)]}) + all_lines_reconciled = fields.Boolean(compute='_check_lines_reconciled') + user_id = fields.Many2one('res.users', string='Responsible', required=False, default=lambda self: self.env.user) + cashbox_start_id = fields.Many2one('account.bank.statement.cashbox') + cashbox_end_id = fields.Many2one('account.bank.statement.cashbox') - def onchange_date(self, cr, uid, ids, date, company_id, context=None): - """ - Find the correct period to use for the given date and company_id, return it and set it in the context - """ - res = {} - period_pool = self.pool.get('account.period') - if context is None: - context = {} - ctx = context.copy() - ctx.update({'company_id': company_id}) - pids = period_pool.find(cr, uid, dt=date, context=ctx) - if pids: - res.update({'period_id': pids[0]}) - context = dict(context, period_id=pids[0]) - return { - 'value':res, - 'context':context, - } + @api.multi + def _balance_check(self): + for stmt in self: + if not stmt.currency_id.is_zero(stmt.difference): + if stmt.journal_type == 'cash': + if stmt.difference < 0.0: + account = stmt.journal_id.loss_account_id + name = _('Loss') + else: + # statement.difference > 0.0 + account = stmt.journal_id.profit_account_id + name = _('Profit') + if not account: + raise UserError(_('There is no account defined on the journal %s for %s involved in a cash difference.') % (stmt.journal_id.name, name)) - def button_dummy(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {}, context=context) - - def _prepare_move(self, cr, uid, st_line, st_line_number, context=None): - """Prepare the dict of values to create the move from a - statement line. This method may be overridden to implement custom - move generation (making sure to call super() to establish - a clean extension chain). - - :param browse_record st_line: account.bank.statement.line record to - create the move from. - :param char st_line_number: will be used as the name of the generated account move - :return: dict of value to create() the account.move - """ - return { - 'statement_line_id': st_line.id, - 'journal_id': st_line.statement_id.journal_id.id, - 'period_id': st_line.statement_id.period_id.id, - 'date': st_line.date, - 'name': st_line_number, - 'ref': st_line.ref, - } - - def _get_counter_part_account(self, cr, uid, st_line, context=None): - """Retrieve the account to use in the counterpart move. - - :param browse_record st_line: account.bank.statement.line record to create the move from. - :return: int/long of the account.account to use as counterpart - """ - if st_line.amount >= 0: - return st_line.statement_id.journal_id.default_credit_account_id.id - return st_line.statement_id.journal_id.default_debit_account_id.id - - def _get_counter_part_partner(self, cr, uid, st_line, context=None): - """Retrieve the partner to use in the counterpart move. - - :param browse_record st_line: account.bank.statement.line record to create the move from. - :return: int/long of the res.partner to use as counterpart - """ - return st_line.partner_id and st_line.partner_id.id or False - - def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency, context=None): - """Compute the args to build the dict of values to create the counter part move line from a - statement line by calling the _prepare_move_line_vals. - - :param browse_record st_line: account.bank.statement.line record to create the move from. - :param int/long move_id: ID of the account.move to link the move line - :param float amount: amount of the move line - :param browse_record company_currency: res.currency record of the concerned company - :return: dict of value to create() the bank account.move.line - """ - currency_obj = self.pool.get('res.currency') - if context is None: - context = {} - ctx = context.copy() - ctx['date'] = st_line.date - account_id = self._get_counter_part_account(cr, uid, st_line, context=context) - partner_id = self._get_counter_part_partner(cr, uid, st_line, context=context) - debit = ((amount > 0) and amount) or 0.0 - credit = ((amount < 0) and -amount) or 0.0 - cur_id = False - amt_cur = False - if st_line.statement_id.currency.id != company_currency.id or (st_line.currency_id and st_line.amount_currency): - #the amount given is in foreign currency and needs to be converted at the ratio of the transaction - st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or 1 - if st_line.currency_id != company_currency: - debit = currency_obj.round(cr, uid, company_currency, debit / st_line_currency_rate) - credit = currency_obj.round(cr, uid, company_currency, credit / st_line_currency_rate) - amt_cur = amount - if st_line.statement_id.currency != company_currency: - amt_cur = amount / st_line_currency_rate - cur_id = st_line.statement_id.currency.id != company_currency.id and st_line.statement_id.currency.id or st_line.currency_id.id - if st_line.statement_id.currency.id != company_currency.id and st_line.currency_id != company_currency: - debit = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, debit, context=ctx) - credit = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, credit, context=ctx) - return self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit, - amount_currency=amt_cur, currency_id=cur_id, account_id=account_id, - partner_id=partner_id, context=context) - - def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id=False, - amount_currency=False, account_id=False, partner_id=False, context=None): - """Prepare the dict of values to create the move line from a - statement line. - - :param browse_record st_line: account.bank.statement.line record to - create the move from. - :param int/long move_id: ID of the account.move to link the move line - :param float debit: debit amount of the move line - :param float credit: credit amount of the move line - :param int/long currency_id: ID of currency of the move line to create - :param float amount_currency: amount of the debit/credit expressed in the currency_id - :param int/long account_id: ID of the account to use in the move line if different - from the statement line account ID - :param int/long partner_id: ID of the partner to put on the move line - :return: dict of value to create() the account.move.line - """ - acc_id = account_id or st_line.account_id.id - cur_id = currency_id or st_line.statement_id.currency.id - par_id = partner_id or (((st_line.partner_id) and st_line.partner_id.id) or False) - return { - 'name': st_line.name, - 'date': st_line.date, - 'ref': st_line.ref, - 'move_id': move_id, - 'partner_id': par_id, - 'account_id': acc_id, - 'credit': credit, - 'debit': debit, - 'statement_id': st_line.statement_id.id, - 'journal_id': st_line.statement_id.journal_id.id, - 'period_id': st_line.statement_id.period_id.id, - 'currency_id': amount_currency and cur_id, - 'amount_currency': amount_currency, - } - - def balance_check(self, cr, uid, st_id, journal_type='bank', context=None): - st = self.browse(cr, uid, st_id, context=context) - if not ((abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001) or (abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001)): - raise UserError(_('The statement balance is incorrect !\nThe expected balance (%.2f) is different than the computed one. (%.2f)') % (st.balance_end_real, st.balance_end)) - return True - - def statement_close(self, cr, uid, ids, journal_type='bank', context=None): - return self.write(cr, uid, ids, {'state':'confirm'}, context=context) - - def check_status_condition(self, cr, uid, state, journal_type='bank'): - return state in ('draft','open') - - def button_confirm_bank(self, cr, uid, ids, context=None): - if context is None: - context = {} - - for st in self.browse(cr, uid, ids, context=context): - j_type = st.journal_id.type - if not self.check_status_condition(cr, uid, st.state, journal_type=j_type): - continue - - self.balance_check(cr, uid, st.id, journal_type=j_type, context=context) - if (not st.journal_id.default_credit_account_id) \ - or (not st.journal_id.default_debit_account_id): - raise UserError(_('Please verify that an account is defined in the journal.')) - for line in st.move_line_ids: - if line.state != 'valid': - raise UserError(_('The account entries lines are not in valid state.')) - move_ids = [] - for st_line in st.line_ids: - if not st_line.amount: - continue - if st_line.account_id and not st_line.journal_entry_ids.ids: - #make an account move as before - vals = { - 'debit': st_line.amount < 0 and -st_line.amount or 0.0, - 'credit': st_line.amount > 0 and st_line.amount or 0.0, - 'account_id': st_line.account_id.id, - 'name': st_line.name + values = { + 'statement_id': stmt.id, + 'account_id': account.id, + 'amount': stmt.difference, + 'name': _("Cash difference observed during the counting (%s)") % name, } - self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context) + self.env['account.bank.statement.line'].create(values) + else: + balance_end_real = formatLang(self.env, stmt.balance_end_real, currency_obj=stmt.currency_id) + balance_end = formatLang(self.env, stmt.balance_end, currency_obj=stmt.currency_id) + raise UserError(_('The ending balance is incorrect !\nThe expected balance (%s) is different from the computed one. (%s)') + % (balance_end_real, balance_end)) + return True + + @api.model + def create(self, vals): + if not vals.get('name'): + journal_id = vals.get('journal_id', self._context.get('default_journal_id', False)) + journal = self.env['account.journal'].browse(journal_id) + vals['name'] = journal.sequence_id.with_context(ir_sequence_date=vals.get('date')).next_by_id() + return super(AccountBankStatement, self).create(vals) + + @api.multi + def unlink(self): + for statement in self: + if statement.state != 'open': + raise UserError(_('In order to delete a bank statement, you must first cancel it to delete related journal items.')) + # Explicitly unlink bank statement lines so it will check that the related journal entries have been deleted first + statement.line_ids.unlink() + return super(AccountBankStatement, self).unlink() + + @api.multi + def open_cashbox_id(self): + context = dict(self.env.context or {}) + if context.get('cashbox_id'): + context['active_id'] = self.id + return { + 'name': _('Cashbox Entries'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'account.bank.statement.cashbox', + 'view_id': self.env.ref('account.view_account_bnk_stmt_cashbox').id, + 'type': 'ir.actions.act_window', + 'res_id': self.env.context.get('cashbox_id'), + 'context': context, + 'target': 'new' + } + + @api.multi + def button_cancel(self): + for statement in self: + if any(line.journal_entry_ids.ids for line in statement.line_ids): + raise UserError(_('A statement cannot be canceled when its lines are reconciled.')) + self.state = 'open' + + @api.multi + def check_confirm_bank(self): + if self.journal_type == 'cash' and not self.currency_id.is_zero(self.difference): + action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_view_account_bnk_stmt_check') + if action_rec: + action = action_rec.read([])[0] + return action + return self.button_confirm_bank() + + @api.multi + def button_confirm_bank(self): + self._balance_check() + statements = self.filtered(lambda r: r.state == 'open') + for statement in statements: + moves = self.env['account.move'] + for st_line in statement.line_ids: + if st_line.account_id and not st_line.journal_entry_ids.ids: + st_line.fast_counterpart_creation() elif not st_line.journal_entry_ids.ids: raise UserError(_('All the account entries lines must be processed in order to close the statement.')) - move_ids += st_line.journal_entry_ids.ids - if move_ids: - self.pool.get('account.move').post(cr, uid, move_ids, context=context) - self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context) - self.link_bank_to_partner(cr, uid, ids, context=context) - return self.write(cr, uid, ids, {'state': 'confirm', 'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context) + moves = (moves | st_line.journal_entry_ids) + if moves: + moves.post() + statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,)) + statements.link_bank_to_partner() + statements.write({'state': 'confirm', 'date_done': time.strftime("%Y-%m-%d %H:%M:%S")}) - def button_cancel(self, cr, uid, ids, context=None): - bnk_st_line_ids = [] - for st in self.browse(cr, uid, ids, context=context): - bnk_st_line_ids += [line.id for line in st.line_ids] - self.pool.get('account.bank.statement.line').cancel(cr, uid, bnk_st_line_ids, context=context) - return self.write(cr, uid, ids, {'state': 'draft'}, context=context) - - def _compute_balance_end_real(self, cr, uid, journal_id, context=None): - res = False - if journal_id: - journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context) - if journal.with_last_closing_balance: - cr.execute('SELECT balance_end_real \ - FROM account_bank_statement \ - WHERE journal_id = %s AND NOT state = %s \ - ORDER BY date DESC,id DESC LIMIT 1', (journal_id, 'draft')) - res = cr.fetchone() - return res and res[0] or 0.0 - - def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None): - if not journal_id: - return {} - balance_start = self._compute_balance_end_real(cr, uid, journal_id, context=context) - journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context) - currency = journal.currency or journal.company_id.currency_id - res = {'balance_start': balance_start, 'company_id': journal.company_id.id, 'currency': currency.id} - if journal.type == 'cash': - res['cash_control'] = journal.cash_control - return {'value': res} - - def unlink(self, cr, uid, ids, context=None): - statement_line_obj = self.pool['account.bank.statement.line'] - for item in self.browse(cr, uid, ids, context=context): - if item.state != 'draft': - raise UserError(_('In order to delete a bank statement, you must first cancel it to delete related journal items.')) - # Explicitly unlink bank statement lines - # so it will check that the related journal entries have - # been deleted first - statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context) - return super(account_bank_statement, self).unlink(cr, uid, ids, context=context) - - def button_journal_entries(self, cr, uid, ids, context=None): - ctx = (context or {}).copy() - ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id + @api.multi + def button_journal_entries(self): + context = dict(self._context or {}) + context['journal_id'] = self.journal_id.id return { 'name': _('Journal Items'), - 'view_type':'form', - 'view_mode':'tree', - 'res_model':'account.move.line', - 'view_id':False, - 'type':'ir.actions.act_window', - 'domain':[('statement_id','in',ids)], - 'context':ctx, + 'view_type': 'form', + 'view_mode': 'tree', + 'res_model': 'account.move.line', + 'view_id': False, + 'type': 'ir.actions.act_window', + 'domain': [('statement_id', 'in', self.ids)], + 'context': context, } - def number_of_lines_reconciled(self, cr, uid, ids, context=None): - bsl_obj = self.pool.get('account.bank.statement.line') - return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_ids', '!=', [])], context=context) + @api.multi + def button_open(self): + """ Changes statement state to Running.""" + for statement in self: + if not statement.name: + context = {'ir_sequence_date', statement.date} + if statement.journal_id.sequence_id: + st_number = statement.journal_id.sequence_id.with_context(context).next_by_id() + else: + SequenceObj = self.env['ir.sequence'] + st_number = SequenceObj.with_context(context).next_by_code('account.bank.statement') + statement.name = st_number + statement.state = 'open' - def link_bank_to_partner(self, cr, uid, ids, context=None): - for statement in self.browse(cr, uid, ids, context=context): + @api.multi + def reconciliation_widget_preprocess(self): + """ Get statement lines of the specified statements or all unreconciled statement lines and try to automatically reconcile them / find them a partner. + Return ids of statement lines left to reconcile and other data for the reconciliation widget. + """ + statements = self + bsl_obj = self.env['account.bank.statement.line'] + + # NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on, + # this is why we filter out statements lines where account_id is set + st_lines_filter = [('journal_entry_ids', '=', False), ('account_id', '=', False)] + if statements: + st_lines_filter += [('statement_id', 'in', statements.ids)] + + # Try to automatically reconcile statement lines + automatic_reconciliation_entries = [] + st_lines_left = self.env['account.bank.statement.line'] + for st_line in bsl_obj.search(st_lines_filter): + res = st_line.auto_reconcile() + if not res: + st_lines_left = (st_lines_left | st_line) + else: + automatic_reconciliation_entries.append(res.ids) + + # Try to set statement line's partner + for st_line in st_lines_left: + if st_line.name and not st_line.partner_id: + additional_domain = [('ref', '=', st_line.name)] + match_recs = st_line.get_move_lines_for_reconciliation(limit=1, additional_domain=additional_domain, overlook_partner=True) + if match_recs and match_recs[0].partner_id: + st_line.write({'partner_id': match_recs[0].partner_id.name}) + + # Collect various informations for the reconciliation widget + notifications = [] + num_auto_reconciled = len(automatic_reconciliation_entries) + if num_auto_reconciled > 0: + auto_reconciled_message = num_auto_reconciled > 1 \ + and _("%d transactions were automatically reconciled.") % num_auto_reconciled \ + or _("1 transaction was automatically reconciled.") + notifications += [{ + 'type': 'info', + 'message': auto_reconciled_message, + 'details': { + 'name': _("Automatically reconciled items"), + 'model': 'account.move', + 'ids': automatic_reconciliation_entries + } + }] + + lines = [] + for el in statements: + lines.extend(el.line_ids.ids) + lines = list(set(lines)) + + return { + 'st_lines_ids': st_lines_left.ids, + 'notifications': notifications, + 'statement_name': len(statements) == 1 and statements[0].name or False, + 'num_already_reconciled_lines': statements and bsl_obj.search_count([('journal_entry_ids', '!=', False), ('id', 'in', lines)]) or 0, + } + + @api.multi + def link_bank_to_partner(self): + for statement in self: for st_line in statement.line_ids: if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id.id != st_line.partner_id.id: - # Update the partner informations of the bank account, possibly overriding existing ones - bank_obj = self.pool.get('res.partner.bank') - bank_vals = bank_obj.onchange_partner_id(cr, uid, [st_line.bank_account_id.id], st_line.partner_id.id, context=context)['value'] + bank_obj = self.env['res.partner.bank'] + bank_vals = bank_obj.onchange_partner_id(self.cr, self.uid, [st_line.bank_account_id.id], st_line.partner_id.id, context=self._context)['value'] bank_vals.update({'partner_id': st_line.partner_id.id}) - bank_obj.write(cr, uid, [st_line.bank_account_id.id], bank_vals, context=context) + st_line.bank_account_id.write(bank_vals) -class account_bank_statement_line(osv.osv): - def create(self, cr, uid, vals, context=None): - if vals.get('amount_currency', 0) and not vals.get('amount', 0): - raise UserError(_('If "Amount Currency" is specified, then "Amount" must be as well.')) - return super(account_bank_statement_line, self).create(cr, uid, vals, context=context) +class AccountBankStatementLine(models.Model): + _name = "account.bank.statement.line" + _description = "Bank Statement Line" + _order = "statement_id desc, sequence" + _inherit = ['ir.needaction_mixin'] - def unlink(self, cr, uid, ids, context=None): - for item in self.browse(cr, uid, ids, context=context): - if item.journal_entry_ids: - raise UserError(_("""In order to delete a bank statement line, you must first cancel it to delete related journal items.""")) - return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context) + name = fields.Char(string='Communication', required=True) + date = fields.Date(required=True, default=lambda self: self._context.get('date', fields.Date.context_today(self))) + amount = fields.Monetary(digits=0, currency_field='company_currency_id') + company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', + help='Utility field to express amount currency', readonly=True) + partner_id = fields.Many2one('res.partner', string='Partner') + bank_account_id = fields.Many2one('res.partner.bank', string='Bank Account') + account_id = fields.Many2one('account.account', string='Counterpart Account', domain=[('deprecated', '=', False)], + help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation" + " process on it later on. The statement line will simply create a counterpart on this account") + statement_id = fields.Many2one('account.bank.statement', string='Statement', index=True, required=True, ondelete='cascade') + journal_id = fields.Many2one('account.journal', related='statement_id.journal_id', string='Journal', store=True, readonly=True) + partner_name = fields.Char(help="This field is used to record the third party name when importing bank statement in electronic format," + " when the partner doesn't exist yet in the database (or cannot be found).") + ref = fields.Char(string='Reference') + note = fields.Text(string='Notes') + sequence = fields.Integer(index=True, help="Gives the sequence order when displaying a list of bank statement lines.", default=1) + company_id = fields.Many2one('res.company', related='statement_id.company_id', string='Company', store=True, readonly=True) + journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True) + amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.") + currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.") - def cancel(self, cr, uid, ids, context=None): - account_move_obj = self.pool.get('account.move') - move_ids = [] - for line in self.browse(cr, uid, ids, context=context): - if line.journal_entry_ids.ids : - move_ids += line.journal_entry_ids.ids - account_move_obj.write(cr, uid, move_ids, {'statement_line_id': False}, context=context) - for move in account_move_obj.browse(cr, uid, move_ids, context=context): - for aml in move.line_id: - if aml.reconcile_id: - move_lines = [l.id for l in aml.reconcile_id.line_id] - move_lines.remove(aml.id) - self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context) - if len(move_lines) >= 2: - self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context) - if move_ids: - account_move_obj.button_cancel(cr, uid, move_ids, context=context) - account_move_obj.unlink(cr, uid, move_ids, context) + @api.one + @api.constrains('amount') + def _check_amount(self): + # This constraint could possibly underline flaws in bank statement import (eg. inability to + # support hacks such as using dummy transactions to give additional informations) + if self.amount == 0: + raise ValidationError(_('A transaction can\'t have a 0 amount.')) - def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None): - """ Returns the data required to display a reconciliation, for each statement line id in ids """ + @api.one + @api.constrains('amount', 'amount_currency') + def _check_amount_currency(self): + if self.amount_currency != 0 and self.amount == 0: + raise ValidationError(_('If "Amount Currency" is specified, then "Amount" must be as well.')) + + @api.multi + def unlink(self): + for line in self: + if line.journal_entry_ids.ids: + raise UserError(_('In order to delete a bank statement line, you must first cancel it to delete related journal items.')) + return super(AccountBankStatementLine, self).unlink() + + @api.model + def _needaction_domain_get(self): + return [('journal_entry_ids', '=', False), ('account_id', '=', False)] + + @api.multi + def button_cancel_reconciliation(self): + # TOCKECK : might not behave as expected in case of reconciliations (match statement line with already + # registered payment) or partial reconciliations : it will completely remove the existing payment. + move_recs = self.env['account.move'] + for st_line in self: + move_recs = (move_recs | st_line.journal_entry_ids) + if move_recs: + for move in move_recs: + move.line_ids.remove_move_reconcile() + move_recs.write({'statement_line_id': False}) + move_recs.button_cancel() + move_recs.unlink() + + #################################################### + # Reconciliation interface methods + #################################################### + + @api.multi + def get_data_for_reconciliation_widget(self, excluded_ids=None): + """ Returns the data required to display a reconciliation widget, for each statement line in self """ + excluded_ids = excluded_ids or [] ret = [] - if excluded_ids is None: - excluded_ids = [] - for st_line in self.browse(cr, uid, ids, context=context): - reconciliation_data = {} - if search_reconciliation_proposition: - reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context) - for mv_line in reconciliation_proposition: - excluded_ids.append(mv_line['id']) - reconciliation_data['reconciliation_proposition'] = reconciliation_proposition - else: - reconciliation_data['reconciliation_proposition'] = [] - st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context) - reconciliation_data['st_line'] = st_line - ret.append(reconciliation_data) + for st_line in self: + aml_recs = st_line.get_reconciliation_proposition(excluded_ids=excluded_ids) + target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id + rp = aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=st_line.date) + excluded_ids += [move_line['id'] for move_line in rp] + ret.append({ + 'st_line': st_line.get_statement_line_for_reconciliation_widget(), + 'reconciliation_proposition': rp + }) return ret - def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None): + def get_statement_line_for_reconciliation_widget(self): """ Returns the data required by the bank statement reconciliation widget to display a statement line """ - if context is None: - context = {} - statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id - rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context) - - if st_line.amount_currency and st_line.currency_id: - amount = st_line.amount_currency - amount_currency = st_line.amount + statement_currency = self.journal_id.currency_id or self.journal_id.company_id.currency_id + if self.amount_currency and self.currency_id: + amount = self.amount_currency + amount_currency = self.amount amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency - amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency) + amount_currency_str = formatLang(self.env, amount_currency_str, currency_obj=statement_currency) else: - amount = st_line.amount + amount = self.amount amount_currency_str = "" - amount_str = amount > 0 and amount or -amount - amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency) + amount_str = formatLang(self.env, abs(amount), currency_obj=self.currency_id or statement_currency) data = { - 'id': st_line.id, - 'ref': st_line.ref, - 'note': st_line.note or "", - 'name': st_line.name, - 'date': st_line.date, + 'id': self.id, + 'ref': self.ref, + 'note': self.note or "", + 'name': self.name, + 'date': self.date, 'amount': amount, - 'amount_str': amount_str, # Amount in the statement line currency - 'currency_id': st_line.currency_id.id or statement_currency.id, - 'partner_id': st_line.partner_id.id, - 'statement_id': st_line.statement_id.id, - 'account_code': st_line.journal_id.default_debit_account_id.code, - 'account_name': st_line.journal_id.default_debit_account_id.name, - 'partner_name': st_line.partner_id.name, - 'communication_partner_name': st_line.partner_name, - 'amount_currency_str': amount_currency_str, # Amount in the statement currency - 'has_no_partner': not st_line.partner_id.id, + 'amount_str': amount_str, # Amount in the statement line currency + 'currency_id': self.currency_id.id or statement_currency.id, + 'partner_id': self.partner_id.id, + 'statement_id': self.statement_id.id, + 'account_code': self.journal_id.default_debit_account_id.code, + 'account_name': self.journal_id.default_debit_account_id.name, + 'partner_name': self.partner_id.name, + 'communication_partner_name': self.partner_name, + 'amount_currency_str': amount_currency_str, # Amount in the statement currency + 'has_no_partner': not self.partner_id.id, } - if st_line.partner_id.id: + if self.partner_id: if amount > 0: - data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id + data['open_balance_account_id'] = self.partner_id.property_account_receivable.id else: - data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id + data['open_balance_account_id'] = self.partner_id.property_account_payable.id return data - def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None): - if excluded_ids is None: - excluded_ids = [] - domain = [('ref', '=', st_line.name), - ('reconcile_id', '=', False), - ('state', '=', 'valid'), - ('account_id.reconcile', '=', True), - ('id', 'not in', excluded_ids)] - return domain + @api.multi + def get_move_lines_for_reconciliation_widget(self, excluded_ids=None, str=False, offset=0, limit=None): + """ Returns move lines for the bank statement reconciliation widget, formatted as a list of dicts + """ + aml_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str, offset=offset, limit=limit) + target_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id + return aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=self.date) - def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None): - """ Returns move lines that constitute the best guess to reconcile a statement line. """ - mv_line_pool = self.pool.get('account.move.line') + #################################################### + # Reconciliation methods + #################################################### - # Look for structured communication - if st_line.name: - domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context) - match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context) - if match_id: - mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context) - target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id - mv_line = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, mv_line_br, target_currency=target_currency, target_date=st_line.date, context=context)[0] - mv_line['has_no_partner'] = not bool(st_line.partner_id.id) - # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner - if (mv_line['partner_id']): - self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context) - mv_line['has_no_partner'] = False - return [mv_line] + def get_move_lines_for_reconciliation(self, excluded_ids=None, str=False, offset=0, limit=None, additional_domain=None, overlook_partner=False): + """ Return account.move.line records which can be used for bank statement reconciliation. - # How to compare statement line amount and move lines amount - precision_digits = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account') - currency_id = st_line.currency_id.id or st_line.journal_id.currency.id - # NB : amount can't be == 0 ; so float precision is not an issue for amount > 0 or amount < 0 - amount = st_line.amount_currency or st_line.amount - domain = [('reconcile_partial_id', '=', False)] - if currency_id: - domain += [('currency_id', '=', currency_id)] - sign = 1 # correct the fact that st_line.amount is signed and debit/credit is not - amount_field = 'debit' - if currency_id == False: - if amount < 0: - amount_field = 'credit' - sign = -1 + :param excluded_ids: + :param str: + :param offset: + :param limit: + :param additional_domain: + :param overlook_partner: + """ + # Domain to fetch registered payments (use case where you encode the payment before you get the bank statement) + reconciliation_aml_accounts = [self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id] + domain_reconciliation = ['&', ('statement_id', '=', False), ('account_id', 'in', reconciliation_aml_accounts)] + + # Domain to fetch unreconciled payables/receivables (use case where you close invoices/refunds by reconciling your bank statements) + domain_matching = [('reconciled', '=', False)] + if self.partner_id.id or overlook_partner: + domain_matching = expression.AND([domain_matching, [('account_id.internal_type', 'in', ['payable', 'receivable'])]]) else: - amount_field = 'amount_currency' + # TODO : find out what use case this permits (match a check payment, registered on a journal whose account type is other instead of liquidity) + domain_matching = expression.AND([domain_matching, [('account_id.reconcile', '=', True)]]) - # Look for a matching amount - domain_exact_amount = domain + [(amount_field, '=', float_round(sign * amount, precision_digits=precision_digits))] - match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=1, additional_domain=domain_exact_amount) - if match_id: - return match_id + # Let's add what applies to both + domain = expression.OR([domain_reconciliation, domain_matching]) + if self.partner_id.id and not overlook_partner: + domain = expression.AND([domain, [('partner_id', '=', self.partner_id.id)]]) - if not st_line.partner_id.id: - return [] + # Domain factorized for all reconciliation use cases + ctx = dict(self._context or {}) + ctx['bank_statement_line'] = self + generic_domain = self.env['account.move.line'].with_context(ctx).domain_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str) + domain = expression.AND([domain, generic_domain]) - # Select move lines until their total amount is greater than the statement line amount - domain += [('account_id.type', 'in', ((amount > 0 and 'receivable' or 'payable'), 'liquidity'))] # Make sure we can't mix receivable and payable - domain += (amount_field == 'amount_currency' and amount < 0) and [(amount_field, '<', 0)] or [(amount_field, '>', 0)] - mv_lines = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, limit=5, additional_domain=domain) - ret = [] - total = 0 - for line in mv_lines: - total += abs(line['debit'] - line['credit']) - ret.append(line) - if float_compare(total, abs(amount), precision_digits=precision_digits) != -1: - break - return ret - - def get_move_lines_for_reconciliation_by_statement_line_id(self, cr, uid, st_line_id, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None): - """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """ - if excluded_ids is None: - excluded_ids = [] - if additional_domain is None: - additional_domain = [] - st_line = self.browse(cr, uid, st_line_id, context=context) - return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context) - - def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None): - if excluded_ids is None: - excluded_ids = [] + # Domain from caller if additional_domain is None: additional_domain = [] else: additional_domain = expression.normalize_domain(additional_domain) + domain = expression.AND([domain, additional_domain]) - # Domain to fetch reconciled move lines (use case where you register a payment before you get the bank statement) - domain_rapprochement = ['&', ('statement_id', '=', False), ('account_id', 'in', [st_line.journal_id.default_credit_account_id.id, st_line.journal_id.default_debit_account_id.id])] + return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") - # Domain to fetch unreconciled move lines (the bank reconciliation process will create a payment journal entry to reconcile with) - domain_reconciliation = [('reconcile_id', '=', False)] - if st_line.partner_id.id: - domain_reconciliation = expression.AND([domain_reconciliation, [('account_id.type', 'in', ['payable', 'receivable'])]]) - else: - domain_reconciliation = expression.AND([domain_reconciliation, [('account_id.reconcile', '=', True)]]) + def _get_domain_maker_move_line_amount(self): + """ Returns a function that can create the appropriate domain to search on move.line amount based on statement.line currency/amount """ + company_currency = self.journal_id.company_id.currency_id + st_line_currency = self.currency_id or self.journal_id.currency_id + currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False + field = currency and 'amount_residual_currency' or 'amount_residual' + precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places - # Let's add what applies to both - domain = expression.OR([domain_rapprochement, domain_reconciliation]) - domain = expression.AND([domain, [('state', '=', 'valid')]]) - if st_line.partner_id.id: - domain = expression.AND([domain, [('partner_id', '=', st_line.partner_id.id)]]) - if excluded_ids: - domain = expression.AND([domain, [('id', 'not in', excluded_ids)]]) - if str: - str_domain = [ - '|', ('move_id.name', 'ilike', str), - '|', ('move_id.ref', 'ilike', str), - '|', ('date_maturity', 'like', str), - '&', ('name', '!=', '/'), ('name', 'ilike', str) - ] - if not st_line.partner_id.id: - str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]]) - domain = expression.AND([domain, str_domain]) + def ret(comparator, amount, p=precision, f=field, c=currency): + if comparator == '<': + if amount < 0: + domain = [(f, '<', 0), (f, '>', amount)] + else: + domain = [(f, '>', 0), (f, '<', amount)] + elif comparator == '=': + domain = [(f, '=', float_round(amount, precision_digits=p))] + else: + raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'")) + domain += [('currency_id', '=', c)] + return domain - return expression.AND([additional_domain, domain]) + return ret - def get_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None): - """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count. - - :param st_line: the browse record of the statement line - :param integers list excluded_ids: ids of move lines that should not be fetched - :param boolean count: just return the number of records - :param tuples list additional_domain: additional domain restrictions + def get_reconciliation_proposition(self, excluded_ids=None): + """ Returns move lines that constitute the best guess to reconcile a statement line + Note: it only looks for move lines in the same currency as the statement line. """ - mv_line_pool = self.pool.get('account.move.line') - domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context) + # Look for structured communication match + if self.name: + overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway + domain = [('ref', '=', self.name)] + match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) + if match_recs and len(match_recs) == 1: + return match_recs - # Get move lines ; in case of a partial reconciliation, only keep one line (the first whose amount is greater than - # the residual amount because it is presumably the invoice, which is the relevant item in this situation) - filtered_lines = [] - reconcile_partial_ids = [] - actual_offset = offset - while True: - line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context) - lines = mv_line_pool.browse(cr, uid, line_ids, context=context) - make_one_more_loop = False - for line in lines: - if line.reconcile_partial_id and \ - (line.reconcile_partial_id.id in reconcile_partial_ids or \ - abs(line.debit - line.credit) < abs(line.amount_residual)): - #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop - #in order to get the right number of items in the pager - make_one_more_loop = True - continue - filtered_lines.append(line) - if line.reconcile_partial_id: - reconcile_partial_ids.append(line.reconcile_partial_id.id) + # How to compare statement line amount and move lines amount + amount_domain_maker = self._get_domain_maker_move_line_amount() + amount = self.amount_currency or self.amount - if not limit or not make_one_more_loop or len(filtered_lines) >= limit: + # Look for a single move line with the same amount + match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=1, additional_domain=amount_domain_maker('=', amount)) + if match_recs: + return match_recs + + if not self.partner_id: + return self.env['account.move.line'] + + # Select move lines until their total amount is greater than the statement line amount + domain = [('reconciled', '=', False)] + domain += [('account_id.user_type.type', '=', amount > 0 and 'receivable' or 'payable')] # Make sure we can't mix receivable and payable + domain += amount_domain_maker('<', amount) # Will also enforce > 0 + mv_lines = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=5, additional_domain=domain) + st_line_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id + ret = self.env['account.move.line'] + total = 0 + for line in mv_lines: + total += line.currency_id and line.amount_residual_currency or line.amount_residual + if float_compare(total, abs(amount), precision_digits=st_line_currency.rounding) != -1: break - actual_offset = actual_offset + limit - lines = limit and filtered_lines[:limit] or filtered_lines + ret = (ret | line) + return ret - # Either return number of lines - if count: - return len(lines) + def _get_move_lines_for_auto_reconcile(self): + """ Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """ + pass - # Or return list of dicts representing the formatted move lines - else: - target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id - mv_lines = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, lines, target_currency=target_currency, target_date=st_line.date, context=context) - has_no_partner = not bool(st_line.partner_id.id) - for line in mv_lines: - line['has_no_partner'] = has_no_partner - return mv_lines - - def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None): - if currency_diff < 0: - account_id = st_line.company_id.expense_currency_exchange_account_id.id - if not account_id: - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_config') - msg = _("You need to configure the 'Loss Exchange Rate Account' in order to manage automatically the booking of accounting entries related to differences between exchange rates.") - raise RedirectWarning(msg, action_id, _('Go to Account Configuration')) - else: - account_id = st_line.company_id.income_currency_exchange_account_id.id - if not account_id: - raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - return { - 'move_id': move_id, - 'name': _('change') + ': ' + (st_line.name or '/'), - 'period_id': st_line.statement_id.period_id.id, - 'journal_id': st_line.journal_id.id, - 'partner_id': st_line.partner_id.id, - 'company_id': st_line.company_id.id, - 'statement_id': st_line.statement_id.id, - 'debit': currency_diff < 0 and -currency_diff or 0, - 'credit': currency_diff > 0 and currency_diff or 0, - 'amount_currency': 0.0, - 'date': st_line.date, - 'account_id': account_id - } - - def process_reconciliations(self, cr, uid, data, context=None): - for datum in data: - self.process_reconciliation(cr, uid, datum[0], datum[1], context=context) - - def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None): - """ Creates a move line for each item of mv_line_dicts and for the statement line. Reconcile a new move line with its counterpart_move_line_id if specified. - Finally, mark the statement line as reconciled by putting the reconciled moves ids in the column journal_entry_ids. - - :param int id: id of the bank statement line - :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it + @api.multi + def auto_reconcile(self): + """ Try to automatically reconcile the statement.line ; return the counterpart journal entry/ies if the automatic reconciliation succeeded, False otherwise. + TODO : this method could be greatly improved and made extensible """ - if context is None: - context = {} - st_line = self.browse(cr, uid, id, context=context) - company_currency = st_line.journal_id.company_id.currency_id - statement_currency = st_line.journal_id.currency or company_currency - bs_obj = self.pool.get('account.bank.statement') - am_obj = self.pool.get('account.move') - aml_obj = self.pool.get('account.move.line') - currency_obj = self.pool.get('res.currency') + self.ensure_one() + match_recs = self.env['account.move.line'] - # Checks - if st_line.journal_entry_ids.ids: + # How to compare statement line amount and move lines amount + amount_domain_maker = self._get_domain_maker_move_line_amount() + equal_amount_domain = amount_domain_maker('=', self.amount_currency or self.amount) + + # Look for structured communication match + if self.name: + overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway + domain = equal_amount_domain + [('ref', '=', self.name)] + match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=domain, overlook_partner=overlook_partner) + if match_recs and len(match_recs) != 1: + return False + + # Look for a single move line with the same partner, the same amount + if not match_recs: + if self.partner_id: + match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=equal_amount_domain) + if match_recs and len(match_recs) != 1: + return False + + if not match_recs: + return False + + # Now reconcile + counterpart_aml_dicts = [] + payment_aml_rec = self.env['account.move.line'] + for aml in match_recs: + if aml.account_id.internal_type == 'liquidity': + payment_aml_rec = (payment_aml_rec | aml) + else: + amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual + counterpart_aml_dicts.append({ + 'name': aml.name if aml.name != '/' else aml.move_id.name, + 'debit': amount < 0 and amount or 0, + 'credit': amount > 0 and -amount or 0, + 'move_line': aml + }) + + try: + with self._cr.savepoint(): + counterpart = self.process_reconciliation(counterpart_aml_dicts=counterpart_aml_dicts, payment_aml_rec=payment_aml_rec) + return counterpart + except UserError: + # A configuration / business logic error that makes it impossible to auto-reconcile should not be raised + # since automatic reconciliation is just an amenity and the user will get the same exception when manually + # reconciling. Other types of exception are (hopefully) programmation errors and should cause a stacktrace. + self.invalidate_cache() + self.env['account.move'].invalidate_cache() + self.env['account.move.line'].invalidate_cache() + return False + + def _prepare_reconciliation_move(self, move_name): + """ Prepare the dict of values to create the move from a statement line. This method may be overridden to adapt domain logic + through model inheritance (make sure to call super() to establish a clean extension chain). + + :param char st_line_number: will be used as the name of the generated account move + :return: dict of value to create() the account.move + """ + return { + 'statement_line_id': self.id, + 'journal_id': self.statement_id.journal_id.id, + 'date': self.date, + 'name': move_name, + 'ref': self.ref, + } + + def _prepare_reconciliation_move_line(self, move, amount): + """ Prepare the dict of values to create the move line from a statement line. + + :param recordset move: the account.move to link the move line + :param float amount: the amount of transaction that wasn't already reconciled + """ + company_currency = self.journal_id.company_id.currency_id + statement_currency = self.journal_id.currency_id or company_currency + st_line_currency = self.currency_id or statement_currency + + amount_currency = False + if statement_currency != company_currency or st_line_currency != company_currency: + # First get the ratio total mount / amount not already reconciled + if statement_currency == company_currency: + total_amount = self.amount + elif st_line_currency == company_currency: + total_amount = self.amount_currency + else: + total_amount = statement_currency.with_context({'date': self.date}).compute(self.amount, company_currency) + ratio = total_amount / amount + # Then use it to adjust the statement.line field that correspond to the move.line amount_currency + if statement_currency != company_currency: + amount_currency = self.amount * ratio + elif st_line_currency != company_currency: + amount_currency = self.amount_currency * ratio + return { + 'name': self.name, + 'date': self.date, + 'ref': self.ref, + 'move_id': move.id, + 'partner_id': self.partner_id and self.partner_id.id or False, + 'account_id': amount >= 0 \ + and self.statement_id.journal_id.default_credit_account_id.id \ + or self.statement_id.journal_id.default_debit_account_id.id, + 'credit': amount < 0 and -amount or 0.0, + 'debit': amount > 0 and amount or 0.0, + 'statement_id': self.statement_id.id, + 'journal_id': self.statement_id.journal_id.id, + 'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False), + 'amount_currency': amount_currency, + } + + @api.v7 + def process_reconciliations(self, cr, uid, ids, data, context=None): + """ Handles data sent from the bank statement reconciliation widget (and can otherwise serve as an old-API bridge) + + :param list of dicts data: must contains the keys 'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts', + whose value is the same as described in process_reconciliation except that ids are used instead of recordsets. + """ + aml_obj = self.pool['account.move.line'] + for id, datum in zip(ids, data): + st_line = self.browse(cr, uid, id, context) + payment_aml_rec = aml_obj.browse(cr, uid, datum.get('payment_aml_ids', []), context) + for aml_dict in datum.get('counterpart_aml_dicts', []): + aml_dict['move_line'] = aml_obj.browse(cr, uid, aml_dict['counterpart_aml_id'], context) + del aml_dict['counterpart_aml_id'] + st_line.process_reconciliation(datum.get('counterpart_aml_dicts', []), payment_aml_rec, datum.get('new_aml_dicts', [])) + + def fast_counterpart_creation(self): + for st_line in self: + # Technical functionality to automatically reconcile by creating a new move line + vals = { + 'name': st_line.name, + 'debit': st_line.amount < 0 and -st_line.amount or 0.0, + 'credit': st_line.amount > 0 and st_line.amount or 0.0, + 'account_id': st_line.account_id.id, + } + st_line.process_reconciliation(new_aml_dicts=[vals]) + + def process_reconciliation(self, counterpart_aml_dicts=None, payment_aml_rec=None, new_aml_dicts=None): + """ Match statement lines with existing payments (eg. checks) and/or payables/receivables (eg. invoices and refunds) and/or new move lines (eg. write-offs). + If any new journal item needs to be created (via new_aml_dicts or counterpart_aml_dicts), a new journal entry will be created and will contain those + items, as well as a journal item for the bank statement line. + Finally, mark the statement line as reconciled by putting the matched moves ids in the column journal_entry_ids. + + :param (list of dicts) counterpart_aml_dicts: move lines to create to reconcile with existing payables/receivables. + The expected keys are : + - 'name' + - 'debit' + - 'credit' + - 'move_line' + # The move line to reconcile (partially if specified debit/credit is lower than move line's credit/debit) + + :param (list of recordsets) payment_aml_rec: recordset move lines representing existing payments (which are already fully reconciled) + + :param (list of dicts) new_aml_dicts: move lines to create. The expected keys are : + - 'name' + - 'debit' + - 'credit' + - 'account_id' + - (optional) 'tax_ids' + - (optional) Other account.move.line fields like analytic_account_id or analytics_id + + :returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains + the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*). + """ + counterpart_aml_dicts = counterpart_aml_dicts or [] + payment_aml_rec = payment_aml_rec or self.env['account.move.line'] + new_aml_dicts = new_aml_dicts or [] + + aml_obj = self.env['account.move.line'] + + company_currency = self.journal_id.company_id.currency_id + statement_currency = self.journal_id.currency_id or company_currency + st_line_currency = self.currency_id or statement_currency + + counterpart_moves = self.env['account.move'] + + # Check and prepare received data + if self.journal_entry_ids.ids: raise UserError(_('The bank statement line was already reconciled.')) - for mv_line_dict in mv_line_dicts: - for field in ['debit', 'credit', 'amount_currency']: - if field not in mv_line_dict: - mv_line_dict[field] = 0.0 - if mv_line_dict.get('counterpart_move_line_id'): - mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context) - if mv_line.reconcile_id: - raise UserError(_('A selected move line was already reconciled.')) + if any(rec.statement_id for rec in payment_aml_rec): + raise UserError(_('A selected move line was already reconciled.')) + for aml_dict in counterpart_aml_dicts: + if aml_dict['move_line'].reconciled: + raise UserError(_('A selected move line was already reconciled.')) + if isinstance(aml_dict['move_line'], (int, long)): + aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line']) + for aml_dict in (counterpart_aml_dicts + new_aml_dicts): + if aml_dict.get('tax_ids') and aml_dict['tax_ids'] and isinstance(aml_dict['tax_ids'][0], (int, long)): + # Transform the value in the format required for One2many and Many2many fields + aml_dict['tax_ids'] = map(lambda id: (4, id, None), aml_dict['tax_ids']) - # When the move line is already paid (fully reconciled move), we just link them to the bank statement - to_reconcile_mv_line_ids = [x['counterpart_move_line_id'] for x in mv_line_dicts if x.get('already_paid', False)] - if to_reconcile_mv_line_ids: - aml_obj.write(cr, uid, to_reconcile_mv_line_ids, {'statement_id': st_line.statement_id.id}, context=context) - for aml in aml_obj.browse(cr, uid, to_reconcile_mv_line_ids, context=context): - am_obj.write(cr, uid, aml.move_id.id, {'statement_line_id': st_line.id}, context=context) + # Fully reconciled moves are just linked to the bank statement + for aml_rec in payment_aml_rec: + aml_rec.write({'statement_id': self.statement_id.id}) + aml_rec.move_id.write({'statement_line_id': self.id}) + counterpart_moves = (counterpart_moves | aml_rec.move_id) - # Otherwise, we create a move line. Either matching an existing journal entry (eg. invoice), in which + # Create move line(s). Either matching an existing journal entry (eg. invoice), in which # case we reconcile the existing and the new move lines together, or being a write-off. - to_create_mv_line_dicts = [x for x in mv_line_dicts if not x.get('already_paid', False)] - if to_create_mv_line_dicts: + if counterpart_aml_dicts or new_aml_dicts: + st_line_currency = self.currency_id or statement_currency + st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False # Create the move - move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence) - move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context) - move_id = am_obj.create(cr, uid, move_vals, context=context) + move_name = (self.statement_id.name or self.name) + "/" + str(self.sequence) + move_vals = self._prepare_reconciliation_move(move_name) + move = self.env['account.move'].create(move_vals) + move.post() + counterpart_moves = (counterpart_moves | move) - # Create the move line for the statement line - st_line_amount = st_line.currency_id and st_line.amount_currency or st_line.amount - if to_reconcile_mv_line_ids: - already_reconciled_amount = reduce(add, [x['credit'] - x['debit'] for x in mv_line_dicts if x.get('already_paid', False)]) - st_line_amount -= already_reconciled_amount - bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, st_line_amount, company_currency, context=context) - aml_obj.create(cr, uid, bank_st_move_vals, context=context) - - # Complete the dicts - st_line_currency = st_line.currency_id or statement_currency - st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or 1 - to_create = [] - for mv_line_dict in to_create_mv_line_dicts: - mv_line_dict.pop('already_paid', None) - mv_line_dict['ref'] = move_name - mv_line_dict['move_id'] = move_id - mv_line_dict['period_id'] = st_line.statement_id.period_id.id - mv_line_dict['journal_id'] = st_line.journal_id.id - mv_line_dict['company_id'] = st_line.company_id.id - mv_line_dict['statement_id'] = st_line.statement_id.id - if mv_line_dict.get('counterpart_move_line_id'): - mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context) - mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id - mv_line_dict['account_id'] = mv_line.account_id.id - if mv_line.currency_id: - ctx = context.copy() - ctx['date'] = mv_line.date - mv_line_dict['amount_currency'] = currency_obj.compute(cr, uid, company_currency.id, mv_line.currency_id.id, mv_line_dict['debit'] - mv_line_dict['credit'], context=ctx) - mv_line_dict['currency_id'] = mv_line.currency_id.id + # Complete dicts to create both counterpart move lines and write-offs + to_create = (counterpart_aml_dicts + new_aml_dicts) + ctx = dict(self._context, date=self.date) + for aml_dict in to_create: + aml_dict['move_id'] = move.id + aml_dict['date'] = self.statement_id.date + aml_dict['partner_id'] = self.partner_id.id + aml_dict['journal_id'] = self.journal_id.id + aml_dict['company_id'] = self.company_id.id + aml_dict['statement_id'] = self.statement_id.id if st_line_currency.id != company_currency.id: - ctx = context.copy() - ctx['date'] = st_line.date - mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit'] - mv_line_dict['currency_id'] = st_line_currency.id - if st_line.currency_id and statement_currency.id == company_currency.id: - #statement is in company currency but the transaction is in foreign currency - debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate) - credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate) - elif st_line.currency_id: - #statement is in foreign currency and the transaction is in another one - debit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['debit'] / st_line_currency_rate, context=ctx) - credit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['credit'] / st_line_currency_rate, context=ctx) + aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit'] + aml_dict['currency_id'] = st_line_currency.id + if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: + # Statement is in company currency but the transaction is in foreign currency + aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate) + aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate) + elif self.currency_id and st_line_currency_rate: + # Statement is in foreign currency and the transaction is in another one + aml_dict['debit'] = statement_currency.with_context(ctx).compute(aml_dict['debit'] / st_line_currency_rate, company_currency) + aml_dict['credit'] = statement_currency.with_context(ctx).compute(aml_dict['credit'] / st_line_currency_rate, company_currency) else: - #statement is in foreign currency and no extra currency is given for the transaction - debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'] / st_line_currency_rate, context=ctx) - credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'] / st_line_currency_rate, context=ctx) - if mv_line_dict.get('counterpart_move_line_id'): - #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line - ctx['date'] = mv_line.date - debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx) - credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx) - mv_line_dict['credit'] = credit_at_old_rate - mv_line_dict['debit'] = debit_at_old_rate - if debit_at_old_rate - debit_at_current_rate: - currency_diff = debit_at_current_rate - debit_at_old_rate - to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context)) - if credit_at_old_rate - credit_at_current_rate: - currency_diff = credit_at_current_rate - credit_at_old_rate - to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) - else: - mv_line_dict['debit'] = debit_at_current_rate - mv_line_dict['credit'] = credit_at_current_rate + # Statement is in foreign currency and no extra currency is given for the transaction + aml_dict['debit'] = st_line_currency.with_context(ctx).compute(aml_dict['debit'], company_currency) + aml_dict['credit'] = st_line_currency.with_context(ctx).compute(aml_dict['credit'], company_currency) elif statement_currency.id != company_currency.id: - #statement is in foreign currency but the transaction is in company currency - prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency - mv_line_dict['amount_currency'] = prorata_factor * st_line.amount - if mv_line_dict.get('counterpart_move_line_id'): - counterpart_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context) - if counterpart_line.currency_id and counterpart_line.currency_id.id == statement_currency.id: - #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line - ctx = context.copy() - ctx['date'] = mv_line.date - debit_at_old_rate = mv_line_dict['amount_currency'] > 0 and currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['amount_currency'], context=ctx) or 0.0 - credit_at_old_rate = mv_line_dict['amount_currency'] < 0 and currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, -mv_line_dict['amount_currency'], context=ctx) or 0.0 - if debit_at_old_rate - mv_line_dict['debit']: - currency_diff = mv_line_dict['debit'] - debit_at_old_rate - to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context)) - if credit_at_old_rate - mv_line_dict['credit']: - currency_diff = mv_line_dict['credit'] - credit_at_old_rate - to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) - mv_line_dict['debit'] = debit_at_old_rate - mv_line_dict['credit'] = credit_at_old_rate - to_create.append(mv_line_dict) + # Statement is in foreign currency but the transaction is in company currency + prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency + aml_dict['amount_currency'] = prorata_factor * self.amount + aml_dict['currency_id'] = statement_currency.id - # If the reconciliation is performed in another currency than the company currency, the amounts are converted to get the right debit/credit. - # If there is more than 1 debit and 1 credit, this can induce a rounding error, which we put in the foreign exchane gain/loss account. - if st_line_currency.id != company_currency.id: - diff_amount = bank_st_move_vals['debit'] - bank_st_move_vals['credit'] \ - + sum(aml['debit'] for aml in to_create) - sum(aml['credit'] for aml in to_create) - if not company_currency.is_zero(diff_amount): - diff_aml = self.get_currency_rate_line(cr, uid, st_line, diff_amount, move_id, context=context) - diff_aml['name'] = _('Rounding error from currency conversion') - to_create.append(diff_aml) + # Create the move line for the statement line using the total credit/debit of the counterpart + # This leaves out the amount already reconciled and avoids rounding errors from currency conversion + st_line_amount = sum(aml_dict['credit'] - aml_dict['debit'] for aml_dict in to_create) + aml_obj.with_context(check_move_validity=False).create(self._prepare_reconciliation_move_line(move, st_line_amount)) - # Create move lines - move_line_pairs_to_reconcile = [] - for mv_line_dict in to_create: - counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict - if mv_line_dict.get('counterpart_move_line_id'): - counterpart_move_line_id = mv_line_dict['counterpart_move_line_id'] - del mv_line_dict['counterpart_move_line_id'] - new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context) - if counterpart_move_line_id != None: - move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id]) + # Create write-offs + for aml_dict in new_aml_dicts: + aml_obj.with_context(check_move_validity=False).create(aml_dict) - # Reconcile - for pair in move_line_pairs_to_reconcile: - aml_obj.reconcile_partial(cr, uid, pair, context=context) + # Create counterpart move lines and reconcile them + for aml_dict in counterpart_aml_dicts: + if aml_dict['move_line'].partner_id.id: + aml_dict['partner_id'] = aml_dict['move_line'].partner_id.id + aml_dict['account_id'] = aml_dict['move_line'].account_id.id - # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just - # return [('journal_entry_ids', '!=', True)] - # Unfortunately, that spawns a "no access rights" error ; it shouldn't. - def _needaction_domain_get(self, cr, uid, context=None): - user = self.pool.get("res.users").browse(cr, uid, uid) - return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_ids', '=', False), ('account_id', '=', False)] + counterpart_move_line = aml_dict.pop('move_line') + if counterpart_move_line.currency_id and counterpart_move_line.currency_id != company_currency and not aml_dict.get('currency_id'): + aml_dict['currency_id'] = counterpart_move_line.currency_id.id + aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], counterpart_move_line.currency_id) + new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict) + (new_aml | counterpart_move_line).reconcile() - _order = "statement_id desc, sequence" - _name = "account.bank.statement.line" - _description = "Bank Statement Line" - _inherit = ['ir.needaction_mixin'] - _columns = { - 'name': fields.char('Communication', required=True), - 'date': fields.date('Date', required=True), - 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')), - 'partner_id': fields.many2one('res.partner', 'Partner'), - 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'), - 'account_id': fields.many2one('account.account', 'Account', help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation process on it later on. The statement line will simply create a counterpart on this account"), - 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'), - 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True), - 'partner_name': fields.char('Partner Name', help="This field is used to record the third party name when importing bank statement in electronic format, when the partner doesn't exist yet in the database (or cannot be found)."), - 'ref': fields.char('Reference'), - 'note': fields.text('Notes'), - 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."), - 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), - 'journal_entry_ids': fields.one2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True), - 'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')), - 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."), - } - _defaults = { - 'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)), - } - -class account_statement_operation_template(osv.osv): - _name = "account.statement.operation.template" - _description = "Preset for the lines that can be created in a bank statement reconciliation" - _columns = { - 'name': fields.char('Button Label', required=True), - 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type', 'not in', ('view', 'closed', 'consolidation'))]), - 'label': fields.char('Journal Item Label'), - 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')], 'Amount type', required=True), - 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), required=True, help="The amount will count as a debit if it is negative, as a credit if it is positive (except if amount type is 'Percentage of open balance')."), - 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='restrict', domain=[('type_tax_use','in',('purchase','all')), ('parent_id','=',False)]), - 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='set null', domain=[('type','!=','view'), ('state','not in',('close','cancelled'))]), - 'company_id': fields.many2one('res.company', 'Company', required=True), - } - _defaults = { - 'amount_type': 'percentage_of_balance', - 'amount': 100.0, - 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - } + counterpart_moves.assert_balanced() + return counterpart_moves diff --git a/addons/account/account_cash_statement.py b/addons/account/account_cash_statement.py deleted file mode 100644 index 04cbbf67aab..00000000000 --- a/addons/account/account_cash_statement.py +++ /dev/null @@ -1,362 +0,0 @@ -# encoding: utf-8 -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2008 PC Solutions (). All Rights Reserved -# $Id$ -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -############################################################################## - -import time - -from openerp.osv import fields, osv -from openerp.tools import float_compare -from openerp.tools.translate import _ -import openerp.addons.decimal_precision as dp -from openerp.exceptions import UserError, AccessError - -class account_cashbox_line(osv.osv): - - """ Cash Box Details """ - - _name = 'account.cashbox.line' - _description = 'CashBox Line' - _rec_name = 'pieces' - - def _sub_total(self, cr, uid, ids, name, arg, context=None): - - """ Calculates Sub total - @param name: Names of fields. - @param arg: User defined arguments - @return: Dictionary of values. - """ - res = {} - for obj in self.browse(cr, uid, ids, context=context): - res[obj.id] = { - 'subtotal_opening' : obj.pieces * obj.number_opening, - 'subtotal_closing' : obj.pieces * obj.number_closing, - } - return res - - def on_change_sub_opening(self, cr, uid, ids, pieces, number, *a): - """ Compute the subtotal for the opening """ - return {'value' : {'subtotal_opening' : (pieces * number) or 0.0 }} - - def on_change_sub_closing(self, cr, uid, ids, pieces, number, *a): - """ Compute the subtotal for the closing """ - return {'value' : {'subtotal_closing' : (pieces * number) or 0.0 }} - - _columns = { - 'pieces': fields.float('Unit of Currency', digits_compute=dp.get_precision('Account')), - 'number_opening' : fields.integer('Number of Units', help='Opening Unit Numbers'), - 'number_closing' : fields.integer('Number of Units', help='Closing Unit Numbers'), - 'subtotal_opening': fields.function(_sub_total, string='Opening Subtotal', type='float', digits_compute=dp.get_precision('Account'), multi='subtotal'), - 'subtotal_closing': fields.function(_sub_total, string='Closing Subtotal', type='float', digits_compute=dp.get_precision('Account'), multi='subtotal'), - 'bank_statement_id' : fields.many2one('account.bank.statement', string="Bank Statements", ondelete='cascade'), - } - - -class account_cash_statement(osv.osv): - - _inherit = 'account.bank.statement' - - def _update_balances(self, cr, uid, ids, context=None): - """ - Set starting and ending balances according to pieces count - """ - res = {} - for statement in self.browse(cr, uid, ids, context=context): - if (statement.journal_id.type not in ('cash',)): - continue - if not statement.journal_id.cash_control: - prec = self.pool['decimal.precision'].precision_get(cr, uid, 'Account') - if float_compare(statement.balance_end_real, statement.balance_end, precision_digits=prec): - statement.write({'balance_end_real' : statement.balance_end}) - continue - start = end = 0 - for line in statement.details_ids: - start += line.subtotal_opening - end += line.subtotal_closing - data = { - 'balance_start': start, - 'balance_end_real': end, - } - res[statement.id] = data - super(account_cash_statement, self).write(cr, uid, [statement.id], data, context=context) - return res - - def _get_sum_entry_encoding(self, cr, uid, ids, name, arg, context=None): - - """ Find encoding total of statements " - @param name: Names of fields. - @param arg: User defined arguments - @return: Dictionary of values. - """ - res = {} - for statement in self.browse(cr, uid, ids, context=context): - res[statement.id] = sum((line.amount for line in statement.line_ids), 0.0) - return res - - def _get_company(self, cr, uid, context=None): - user_pool = self.pool.get('res.users') - company_pool = self.pool.get('res.company') - user = user_pool.browse(cr, uid, uid, context=context) - company_id = user.company_id - if not company_id: - company_id = company_pool.search(cr, uid, []) - return company_id and company_id[0] or False - - def _get_statement_from_line(self, cr, uid, ids, context=None): - result = {} - for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context): - result[line.statement_id.id] = True - return result.keys() - - def _compute_difference(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, 0.0) - - for obj in self.browse(cr, uid, ids, context=context): - result[obj.id] = obj.balance_end_real - obj.balance_end - - return result - - def _compute_last_closing_balance(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, 0.0) - - for obj in self.browse(cr, uid, ids, context=context): - if obj.state == 'draft': - statement_ids = self.search(cr, uid, - [('journal_id', '=', obj.journal_id.id),('state', '=', 'confirm')], - order='create_date desc', - limit=1, - context=context - ) - - if not statement_ids: - continue - else: - st = self.browse(cr, uid, statement_ids[0], context=context) - result[obj.id] = st.balance_end_real - - return result - - def onchange_journal_id(self, cr, uid, ids, journal_id, context=None): - result = super(account_cash_statement, self).onchange_journal_id(cr, uid, ids, journal_id) - - if not journal_id: - return result - - statement_ids = self.search(cr, uid, - [('journal_id', '=', journal_id),('state', '=', 'confirm')], - order='create_date desc', - limit=1, - context=context - ) - - opening_details_ids = self._get_cash_open_box_lines(cr, uid, journal_id, context) - if opening_details_ids: - result['value']['opening_details_ids'] = opening_details_ids - - if not statement_ids: - return result - - st = self.browse(cr, uid, statement_ids[0], context=context) - result.setdefault('value', {}).update({'last_closing_balance' : st.balance_end_real}) - - return result - - _columns = { - 'total_entry_encoding': fields.function(_get_sum_entry_encoding, string="Total Transactions", - store = { - 'account.bank.statement': (lambda self, cr, uid, ids, context=None: ids, ['line_ids','move_line_ids'], 10), - 'account.bank.statement.line': (_get_statement_from_line, ['amount'], 10), - }, - help="Total of cash transaction lines."), - 'closing_date': fields.datetime("Closed On"), - 'details_ids' : fields.one2many('account.cashbox.line', 'bank_statement_id', string='CashBox Lines', copy=True), - 'opening_details_ids' : fields.one2many('account.cashbox.line', 'bank_statement_id', string='Opening Cashbox Lines'), - 'closing_details_ids' : fields.one2many('account.cashbox.line', 'bank_statement_id', string='Closing Cashbox Lines'), - 'user_id': fields.many2one('res.users', 'Responsible', required=False), - 'difference' : fields.function(_compute_difference, method=True, string="Difference", type="float", help="Difference between the theoretical closing balance and the real closing balance."), - 'last_closing_balance' : fields.function(_compute_last_closing_balance, method=True, string='Last Closing Balance', type='float'), - } - _defaults = { - 'state': 'draft', - 'date': lambda self, cr, uid, context={}: context.get('date', time.strftime("%Y-%m-%d %H:%M:%S")), - 'user_id': lambda self, cr, uid, context=None: uid, - } - - def _get_cash_open_box_lines(self, cr, uid, journal_id, context): - details_ids = [] - if not journal_id: - return details_ids - journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context) - if journal and (journal.type == 'cash'): - last_pieces = None - - if journal.with_last_closing_balance == True: - domain = [('journal_id', '=', journal.id), - ('state', '=', 'confirm')] - last_bank_statement_ids = self.search(cr, uid, domain, limit=1, order='create_date desc', context=context) - if last_bank_statement_ids: - last_bank_statement = self.browse(cr, uid, last_bank_statement_ids[0], context=context) - - last_pieces = dict( - (line.pieces, line.number_closing) for line in last_bank_statement.details_ids - ) - for value in journal.cashbox_line_ids: - nested_values = { - 'number_closing' : 0, - 'number_opening' : last_pieces.get(value.pieces, 0) if isinstance(last_pieces, dict) else 0, - 'pieces' : value.pieces - } - details_ids.append([0, False, nested_values]) - return details_ids - - def create(self, cr, uid, vals, context=None): - journal_id = vals.get('journal_id') - if journal_id and not vals.get('opening_details_ids'): - vals['opening_details_ids'] = vals.get('opening_details_ids') or self._get_cash_open_box_lines(cr, uid, journal_id, context) - res_id = super(account_cash_statement, self).create(cr, uid, vals, context=context) - self._update_balances(cr, uid, [res_id], context) - return res_id - - def write(self, cr, uid, ids, vals, context=None): - """ - Update redord(s) comes in {ids}, with new value comes as {vals} - return True on success, False otherwise - - @param cr: cursor to database - @param user: id of current user - @param ids: list of record ids to be update - @param vals: dict of new values to be set - @param context: context arguments, like lang, time zone - - @return: True on success, False otherwise - """ - if vals.get('journal_id', False): - cashbox_line_obj = self.pool.get('account.cashbox.line') - cashbox_ids = cashbox_line_obj.search(cr, uid, [('bank_statement_id', 'in', ids)], context=context) - cashbox_line_obj.unlink(cr, uid, cashbox_ids, context) - res = super(account_cash_statement, self).write(cr, uid, ids, vals, context=context) - self._update_balances(cr, uid, ids, context) - return res - - def _user_allow(self, cr, uid, statement_id, context=None): - return True - - def button_open(self, cr, uid, ids, context=None): - """ Changes statement state to Running. - @return: True - """ - obj_seq = self.pool.get('ir.sequence') - if context is None: - context = {} - statement_pool = self.pool.get('account.bank.statement') - for statement in statement_pool.browse(cr, uid, ids, context=context): - vals = {} - if not self._user_allow(cr, uid, statement.id, context=context): - raise AccessError((_('You do not have rights to open this %s journal!') % (statement.journal_id.name, ))) - - if statement.name and statement.name == '/': - c = {'ir_sequence_date': statement.period_id.date_start} - if statement.journal_id.sequence_id: - st_number = obj_seq.next_by_id(cr, uid, statement.journal_id.sequence_id.id, context=c) - else: - st_number = obj_seq.next_by_code(cr, uid, 'account.cash.statement', context=c) - vals.update({ - 'name': st_number - }) - - vals.update({ - 'state': 'open', - }) - self.write(cr, uid, [statement.id], vals, context=context) - return True - - def statement_close(self, cr, uid, ids, journal_type='bank', context=None): - if journal_type == 'bank': - return super(account_cash_statement, self).statement_close(cr, uid, ids, journal_type, context) - vals = { - 'state':'confirm', - 'closing_date': time.strftime("%Y-%m-%d %H:%M:%S") - } - return self.write(cr, uid, ids, vals, context=context) - - def check_status_condition(self, cr, uid, state, journal_type='bank'): - if journal_type == 'bank': - return super(account_cash_statement, self).check_status_condition(cr, uid, state, journal_type) - return state=='open' - - def button_confirm_cash(self, cr, uid, ids, context=None): - absl_proxy = self.pool.get('account.bank.statement.line') - - TABLES = ((_('Profit'), 'profit_account_id'), (_('Loss'), 'loss_account_id'),) - - for obj in self.browse(cr, uid, ids, context=context): - if obj.difference == 0.0: - continue - elif obj.difference < 0.0: - account = obj.journal_id.loss_account_id - name = _('Loss') - if not obj.journal_id.loss_account_id: - raise UserError(_('There is no Loss Account on the journal %s.') % (obj.journal_id.name,)) - else: # obj.difference > 0.0 - account = obj.journal_id.profit_account_id - name = _('Profit') - if not obj.journal_id.profit_account_id: - raise UserError(_('There is no Profit Account on the journal %s.') % (obj.journal_id.name,)) - - values = { - 'statement_id' : obj.id, - 'journal_id' : obj.journal_id.id, - 'account_id' : account.id, - 'amount' : obj.difference, - 'name' : name, - } - absl_proxy.create(cr, uid, values, context=context) - - return super(account_cash_statement, self).button_confirm_bank(cr, uid, ids, context=context) - - -class account_journal(osv.osv): - _inherit = 'account.journal' - - def _default_cashbox_line_ids(self, cr, uid, context=None): - # Return a list of coins in Euros. - result = [ - dict(pieces=value) for value in [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500] - ] - return result - - _columns = { - 'cashbox_line_ids' : fields.one2many('account.journal.cashbox.line', 'journal_id', 'CashBox', copy=True), - } - - _defaults = { - 'cashbox_line_ids' : _default_cashbox_line_ids, - } - - -class account_journal_cashbox_line(osv.osv): - _name = 'account.journal.cashbox.line' - _rec_name = 'pieces' - _columns = { - 'pieces': fields.float('Values', digits_compute=dp.get_precision('Account')), - 'journal_id' : fields.many2one('account.journal', 'Journal', required=True, select=1, ondelete="cascade"), - } - - _order = 'pieces asc' diff --git a/addons/account/account_end_fy.xml b/addons/account/account_end_fy.xml deleted file mode 100644 index ecfc1c813b1..00000000000 --- a/addons/account/account_end_fy.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Close a Period - account.period - form - - {'search_default_draft': 1} - A period is a fiscal period of time during which accounting entries should be recorded for accounting related activities. Monthly period is the norm but depending on your countries or company needs, you could also have quarterly periods. Closing a period will make it impossible to record new accounting entries, all new entries should then be made on the following open period. Close a period when you do not want to record new entries and want to lock this period for tax related calculation. - - - - - diff --git a/addons/account/account_financial_report.py b/addons/account/account_financial_report.py deleted file mode 100644 index 66cce7be4b3..00000000000 --- a/addons/account/account_financial_report.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 time -from datetime import datetime -from dateutil.relativedelta import relativedelta -from operator import itemgetter - -from openerp.osv import fields, osv -import openerp.addons.decimal_precision as dp -from openerp.tools.translate import _ - -# --------------------------------------------------------- -# Account Financial Report -# --------------------------------------------------------- - -class account_financial_report(osv.osv): - _name = "account.financial.report" - _description = "Account Report" - - def _get_level(self, cr, uid, ids, field_name, arg, context=None): - '''Returns a dictionary with key=the ID of a record and value = the level of this - record in the tree structure.''' - res = {} - for report in self.browse(cr, uid, ids, context=context): - level = 0 - if report.parent_id: - level = report.parent_id.level + 1 - res[report.id] = level - return res - - def _get_children_by_order(self, cr, uid, ids, context=None): - '''returns a dictionary with the key= the ID of a record and value = all its children, - computed recursively, and sorted by sequence. Ready for the printing''' - res = [] - for id in ids: - res.append(id) - ids2 = self.search(cr, uid, [('parent_id', '=', id)], order='sequence ASC', context=context) - res += self._get_children_by_order(cr, uid, ids2, context=context) - return res - - def _get_balance(self, cr, uid, ids, field_names, args, context=None): - '''returns a dictionary with key=the ID of a record and value=the balance amount - computed for this record. If the record is of type : - 'accounts' : it's the sum of the linked accounts - 'account_type' : it's the sum of leaf accoutns with such an account_type - 'account_report' : it's the amount of the related report - 'sum' : it's the sum of the children of this record (aka a 'view' record)''' - account_obj = self.pool.get('account.account') - res = {} - for report in self.browse(cr, uid, ids, context=context): - if report.id in res: - continue - res[report.id] = dict((fn, 0.0) for fn in field_names) - if report.type == 'accounts': - # it's the sum of the linked accounts - for a in report.account_ids: - for field in field_names: - res[report.id][field] += getattr(a, field) - elif report.type == 'account_type': - # it's the sum the leaf accounts with such an account type - report_types = [x.id for x in report.account_type_ids] - account_ids = account_obj.search(cr, uid, [('user_type','in', report_types), ('type','!=','view')], context=context) - for a in account_obj.browse(cr, uid, account_ids, context=context): - for field in field_names: - res[report.id][field] += getattr(a, field) - elif report.type == 'account_report' and report.account_report_id: - # it's the amount of the linked report - res2 = self._get_balance(cr, uid, [report.account_report_id.id], field_names, False, context=context) - for key, value in res2.items(): - for field in field_names: - res[report.id][field] += value[field] - elif report.type == 'sum': - # it's the sum of the children of this account.report - res2 = self._get_balance(cr, uid, [rec.id for rec in report.children_ids], field_names, False, context=context) - for key, value in res2.items(): - for field in field_names: - res[report.id][field] += value[field] - return res - - _columns = { - 'name': fields.char('Report Name', required=True, translate=True), - 'parent_id': fields.many2one('account.financial.report', 'Parent'), - 'children_ids': fields.one2many('account.financial.report', 'parent_id', 'Account Report'), - 'sequence': fields.integer('Sequence'), - 'balance': fields.function(_get_balance, string='Balance', multi='balance'), - 'debit': fields.function(_get_balance, string='Debit', multi='balance'), - 'credit': fields.function(_get_balance, string='Credit', multi="balance"), - 'level': fields.function(_get_level, string='Level', store=True, type='integer'), - 'type': fields.selection([ - ('sum','View'), - ('accounts','Accounts'), - ('account_type','Account Type'), - ('account_report','Report Value'), - ],'Type'), - 'account_ids': fields.many2many('account.account', 'account_account_financial_report', 'report_line_id', 'account_id', 'Accounts'), - 'account_report_id': fields.many2one('account.financial.report', 'Report Value'), - 'account_type_ids': fields.many2many('account.account.type', 'account_account_financial_report_type', 'report_id', 'account_type_id', 'Account Types'), - 'sign': fields.selection([(-1, 'Reverse balance sign'), (1, 'Preserve balance sign')], 'Sign on Reports', required=True, help='For accounts that are typically more debited than credited and that you would like to print as negative amounts in your reports, you should reverse the sign of the balance; e.g.: Expense account. The same applies for accounts that are typically more credited than debited and that you would like to print as positive amounts in your reports; e.g.: Income account.'), - 'display_detail': fields.selection([ - ('no_detail','No detail'), - ('detail_flat','Display children flat'), - ('detail_with_hierarchy','Display children with hierarchy') - ], 'Display details'), - 'style_overwrite': fields.selection([ - (0, 'Automatic formatting'), - (1,'Main Title 1 (bold, underlined)'), - (2,'Title 2 (bold)'), - (3,'Title 3 (bold, smaller)'), - (4,'Normal Text'), - (5,'Italic Text (smaller)'), - (6,'Smallest Text'), - ],'Financial Report Style', help="You can set up here the format you want this record to be displayed. If you leave the automatic formatting, it will be computed based on the financial reports hierarchy (auto-computed field 'level')."), - } - - _defaults = { - 'type': 'sum', - 'display_detail': 'detail_flat', - 'sign': 1, - 'style_overwrite': 0, - } diff --git a/addons/account/account_financial_report_data.xml b/addons/account/account_financial_report_data.xml deleted file mode 100644 index e8ff33151c3..00000000000 --- a/addons/account/account_financial_report_data.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - Profit and Loss - - sum - - - Income - - - detail_with_hierarchy - account_type - - - Expense - - - detail_with_hierarchy - account_type - - - - Balance Sheet - sum - - - Assets - - detail_with_hierarchy - account_type - - - Liability - - no_detail - sum - - - Liability - - detail_with_hierarchy - account_type - - - Profit (Loss) to report - - no_detail - account_report - - - - - diff --git a/addons/account/account_installer.xml b/addons/account/account_installer.xml deleted file mode 100644 index 2d9625447ed..00000000000 --- a/addons/account/account_installer.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - account.installer.form - account.installer - - -
- Accounting Application Configuration -
-
-
-
-
- -

- Select a configuration package to setup automatically your - taxes and chart of accounts. -

- - - - - - - - -
-
-
- - - Configure Accounting Data - ir.actions.act_window - account.installer - - form - form - new - - - - - 3 - automatic - - -
-
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 751f7cf196d..cca03fed132 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1,44 +1,29 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 itertools +import json from lxml import etree +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from openerp import api, fields, models, _ +from openerp.tools import float_is_zero -from openerp import models, fields, api, _ from openerp.exceptions import UserError, RedirectWarning -from openerp.tools import float_compare + import openerp.addons.decimal_precision as dp # mapping invoice type to journal type TYPE2JOURNAL = { 'out_invoice': 'sale', 'in_invoice': 'purchase', - 'out_refund': 'sale_refund', - 'in_refund': 'purchase_refund', + 'out_refund': 'sale', + 'in_refund': 'purchase', } # mapping invoice type to refund type TYPE2REFUND = { 'out_invoice': 'out_refund', # Customer Invoice - 'in_invoice': 'in_refund', # Supplier Invoice + 'in_invoice': 'in_refund', # Supplier Bill 'out_refund': 'out_invoice', # Customer Refund 'in_refund': 'in_invoice', # Supplier Refund } @@ -46,18 +31,26 @@ TYPE2REFUND = { MAGIC_COLUMNS = ('id', 'create_uid', 'create_date', 'write_uid', 'write_date') -class account_invoice(models.Model): +class AccountInvoice(models.Model): _name = "account.invoice" _inherit = ['mail.thread'] _description = "Invoice" - _order = "number desc, id desc" + _order = "date_invoice desc, number desc, id desc" @api.one - @api.depends('invoice_line.price_subtotal', 'tax_line.amount') + @api.depends('invoice_line_ids.price_subtotal', 'tax_line_ids.amount', 'currency_id', 'company_id') def _compute_amount(self): - self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line) - self.amount_tax = sum(line.amount for line in self.tax_line) + self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line_ids) + self.amount_tax = sum(line.amount for line in self.tax_line_ids) self.amount_total = self.amount_untaxed + self.amount_tax + amount_total_signed = self.amount_total + amount_untaxed_signed = self.amount_untaxed + if self.currency_id and self.currency_id != self.company_id.currency_id: + amount_total_signed = self.currency_id.compute(self.amount_total, self.company_id.currency_id) + amount_untaxed_signed = self.currency_id.compute(self.amount_untaxed, self.company_id.currency_id) + sign = self.type in ['in_refund', 'out_refund'] and -1 or 1 + self.amount_total_signed = amount_total_signed * sign + self.amount_untaxed_signed = amount_untaxed_signed * sign @api.model def _default_journal(self): @@ -73,23 +66,17 @@ class account_invoice(models.Model): @api.model def _default_currency(self): journal = self._default_journal() - return journal.currency or journal.company_id.currency_id + return journal.currency_id or journal.company_id.currency_id @api.model @api.returns('account.analytic.journal', lambda r: r.id) def _get_journal_analytic(self, inv_type): """ Return the analytic journal corresponding to the given invoice type. """ - type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'} - journal_type = type2journal.get(inv_type, 'sale') + journal_type = TYPE2JOURNAL.get(inv_type, 'sale') journal = self.env['account.analytic.journal'].search([('type', '=', journal_type)], limit=1) if not journal: raise UserError(_("You must define an analytic journal of type '%s'!") % (journal_type,)) - return journal[0] - - @api.one - @api.depends('account_id', 'move_id.line_id.account_id', 'move_id.line_id.reconcile_id') - def _compute_reconciled(self): - self.reconciled = self.test_paid() + return journal @api.model def _get_reference_type(self): @@ -97,109 +84,118 @@ class account_invoice(models.Model): @api.one @api.depends( - 'state', 'currency_id', 'invoice_line.price_subtotal', - 'move_id.line_id.account_id.type', - 'move_id.line_id.amount_residual', - # Fixes the fact that move_id.line_id.amount_residual, being not stored and old API, doesn't trigger recomputation - 'move_id.line_id.reconcile_id', - 'move_id.line_id.amount_residual_currency', - 'move_id.line_id.currency_id', - 'move_id.line_id.reconcile_partial_id.line_partial_ids.invoice.type', - ) - # An invoice's residual amount is the sum of its unreconciled move lines and, - # for partially reconciled move lines, their residual amount divided by the - # number of times this reconciliation is used in an invoice (so we split - # the residual amount between all invoice) + 'state', 'currency_id', 'invoice_line_ids.price_subtotal', + 'move_id.line_ids.amount_residual', + 'move_id.line_ids.currency_id') def _compute_residual(self): - self.residual = 0.0 - # Each partial reconciliation is considered only once for each invoice it appears into, - # and its residual amount is divided by this number of invoices - partial_reconciliations_done = [] - for line in self.sudo().move_id.line_id: - if line.account_id.type not in ('receivable', 'payable'): - continue - if line.reconcile_partial_id and line.reconcile_partial_id.id in partial_reconciliations_done: - continue - # Get the correct line residual amount - if line.currency_id == self.currency_id: - line_amount = line.currency_id and line.amount_residual_currency or line.amount_residual - else: - from_currency = line.company_id.currency_id.with_context(date=line.date) - line_amount = from_currency.compute(line.amount_residual, self.currency_id) - # For partially reconciled lines, split the residual amount - if line.reconcile_partial_id: - partial_reconciliation_invoices = set() - for pline in line.reconcile_partial_id.line_partial_ids: - if pline.invoice and self.type == pline.invoice.type: - partial_reconciliation_invoices.update([pline.invoice.id]) - line_amount = self.currency_id.round(line_amount / len(partial_reconciliation_invoices)) - partial_reconciliations_done.append(line.reconcile_partial_id.id) - self.residual += line_amount - self.residual = max(self.residual, 0.0) + residual = 0.0 + residual_signed = 0.0 + sign = self.type in ['in_refund', 'in_invoice'] and -1 or 1 + for line in self.sudo().move_id.line_ids: + if line.account_id.internal_type in ('receivable', 'payable'): + residual_signed += line.amount_residual * sign + if line.currency_id == self.currency_id: + residual += line.currency_id and line.amount_residual_currency or line.amount_residual + else: + from_currency = (line.currency_id and line.currency_id.with_context(date=line.date)) or line.company_id.currency_id.with_context(date=line.date) + residual += from_currency.compute(line.amount_residual, self.currency_id) + self.residual_signed = residual_signed + self.residual = abs(residual) + digits_rounding_precision = self.currency_id.rounding + if float_is_zero(self.residual, digits_rounding_precision): + self.reconciled = True @api.one - @api.depends( - 'move_id.line_id.account_id', - 'move_id.line_id.reconcile_id.line_id', - 'move_id.line_id.reconcile_partial_id.line_partial_ids', - ) - def _compute_move_lines(self): - # Give Journal Items related to the payment reconciled to this invoice. - # Return partial and total payments related to the selected invoice. - self.move_lines = self.env['account.move.line'] - if not self.move_id: - return - data_lines = self.move_id.line_id.filtered(lambda l: l.account_id == self.account_id) - partial_lines = self.env['account.move.line'] - for data_line in data_lines: - if data_line.reconcile_id: - lines = data_line.reconcile_id.line_id - elif data_line.reconcile_partial_id: - lines = data_line.reconcile_partial_id.line_partial_ids + def _get_outstanding_info_JSON(self): + self.outstanding_credits_debits_widget = json.dumps(False) + if self.state == 'open': + domain = [('journal_id.type', 'in', ('bank', 'cash')), ('account_id', '=', self.account_id.id), ('partner_id', '=', self.partner_id.id), ('reconciled', '=', False), ('amount_residual', '!=', 0.0)] + if self.type in ('out_invoice', 'in_refund'): + domain.extend([('credit', '>', 0), ('debit', '=', 0)]) + type_payment = _('Outstanding credit from %s') else: - lines = self.env['account.move.line'] - partial_lines += data_line - self.move_lines = lines - partial_lines + domain.extend([('credit', '=', 0), ('debit', '>', 0)]) + type_payment = _('Outstanding debit from %s') + info = {'title': '', 'outstanding': True, 'content': [], 'invoice_id': self.id} + lines = self.env['account.move.line'].search(domain) + if len(lines) != 0: + for line in lines: + # get the outstanding residual value in invoice currency + if line.currency_id: + amount_to_show = line.currency_id.compute(abs(line.amount_residual_currency), self.currency_id) + else: + amount_to_show = line.company_id.currency_id.compute(abs(line.amount_residual), self.currency_id) + info['content'].append({ + 'ref': line.ref or line.move_id.name, + 'amount': amount_to_show, + 'currency': self.currency_id.symbol, + 'id': line.id, + 'position': self.currency_id.position, + 'digits': [69, self.currency_id.decimal_places], + }) + info['title'] = type_payment % line.partner_id.name + self.outstanding_credits_debits_widget = json.dumps(info) + self.has_outstanding = True @api.one - @api.depends( - 'move_id.line_id.reconcile_id.line_id', - 'move_id.line_id.reconcile_partial_id.line_partial_ids', - ) + def _get_payment_info_JSON(self): + self.payments_widget = json.dumps(False) + if self.payment_move_line_ids: + info = {'title': _('Less Payment'), 'outstanding': False, 'content': []} + for payment in self.payment_move_line_ids: + if self.type in ('out_invoice', 'in_refund'): + amount = sum([p.amount for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids]) + amount_currency = sum([p.amount_currency for p in payment.matched_debit_ids if p.debit_move_id in self.move_id.line_ids]) + elif self.type in ('in_invoice', 'out_refund'): + amount = sum([p.amount for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids]) + amount_currency = sum([p.amount_currency for p in payment.matched_credit_ids if p.credit_move_id in self.move_id.line_ids]) + #we don't take into account the movement created due to a change difference + if payment.currency_id and payment.move_id.rate_diff_partial_rec_id: + continue + # get the payment value in invoice currency + if payment.currency_id and amount_currency != 0: + amount_to_show = amount_currency + else: + amount_to_show = payment.company_id.currency_id.compute(-amount, self.currency_id) + info['content'].append({ + 'name': payment.name, + 'ref': payment.journal_id.name, + 'amount': amount_to_show, + 'currency': self.currency_id.symbol, + 'digits': [69, self.currency_id.decimal_places], + 'position': self.currency_id.position, + 'date': payment.date, + }) + self.payments_widget = json.dumps(info) + + @api.one + @api.depends('move_id.line_ids.amount_residual') def _compute_payments(self): - partial_lines = lines = self.env['account.move.line'] - for line in self.move_id.line_id: - if line.account_id != self.account_id: - continue - if line.reconcile_id: - lines |= line.reconcile_id.line_id - elif line.reconcile_partial_id: - lines |= line.reconcile_partial_id.line_partial_ids - partial_lines += line - self.payment_ids = (lines - partial_lines).sorted() + payment_lines = [] + for line in self.move_id.line_ids: + payment_lines.extend([rp.credit_move_id.id for rp in line.matched_credit_ids]) + payment_lines.extend([rp.debit_move_id.id for rp in line.matched_debit_ids]) + self.payment_move_line_ids = self.env['account.move.line'].browse(payment_lines).sorted() name = fields.Char(string='Reference/Description', index=True, - readonly=True, states={'draft': [('readonly', False)]}) + readonly=True, states={'draft': [('readonly', False)]}, copy=False) origin = fields.Char(string='Source Document', help="Reference of the document that produced this invoice.", readonly=True, states={'draft': [('readonly', False)]}) - supplier_invoice_number = fields.Char(string='Supplier Invoice Number', - help="The reference of this invoice as provided by the supplier.", - readonly=True, states={'draft': [('readonly', False)]}) type = fields.Selection([ ('out_invoice','Customer Invoice'), - ('in_invoice','Supplier Invoice'), + ('in_invoice','Supplier Bill'), ('out_refund','Customer Refund'), ('in_refund','Supplier Refund'), - ], string='Type', readonly=True, index=True, change_default=True, + ], readonly=True, index=True, change_default=True, default=lambda self: self._context.get('type', 'out_invoice'), track_visibility='always') number = fields.Char(related='move_id.name', store=True, readonly=True, copy=False) - internal_number = fields.Char(string='Invoice Number', readonly=True, + move_name = fields.Char(string='Journal Entry', readonly=True, default=False, copy=False, - help="Unique number of the invoice, computed automatically when the invoice is created.") - reference = fields.Char(string='Invoice Reference', + help="Technical field holding the number given to the invoice, automatically set when the invoice is validated then stored to set the same number again if the invoice is cancelled, set to draft and re-validated.") + reference = fields.Char(string='Partner\'s Invoice Reference', help="The partner reference of this invoice.") reference_type = fields.Selection('_get_reference_type', string='Payment Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, @@ -208,16 +204,16 @@ class account_invoice(models.Model): state = fields.Selection([ ('draft','Draft'), - ('proforma','Pro-forma'), - ('proforma2','Pro-forma'), - ('open','Open'), - ('paid','Paid'), - ('cancel','Cancelled'), + ('proforma', 'Pro-forma'), + ('proforma2', 'Pro-forma'), + ('open', 'Open'), + ('paid', 'Paid'), + ('cancel', 'Cancelled'), ], string='Status', index=True, readonly=True, default='draft', track_visibility='onchange', copy=False, help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n" - " * The 'Pro-forma' when invoice is in Pro-forma status,invoice does not have an invoice number.\n" - " * The 'Open' status is used when user create invoice,a invoice number is generated.Its in open status till user does not pay invoice.\n" + " * The 'Pro-forma' when invoice is in Pro-forma status, invoice does not have an invoice number.\n" + " * The 'Open' status is used when user create invoice, an invoice number is generated. Its in open status till user does not pay invoice.\n" " * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n" " * The 'Cancelled' status is used when user cancel invoice.") sent = fields.Boolean(readonly=True, default=False, copy=False, @@ -235,93 +231,97 @@ class account_invoice(models.Model): partner_id = fields.Many2one('res.partner', string='Partner', change_default=True, required=True, readonly=True, states={'draft': [('readonly', False)]}, track_visibility='always') - payment_term = fields.Many2one('account.payment.term', string='Payment Terms', + payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', oldname='payment_term', readonly=True, states={'draft': [('readonly', False)]}, help="If you use payment terms, the due date will be computed automatically at the generation " "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. " "The payment term may compute several due dates, for example 50% now, 50% in one month.") - period_id = fields.Many2one('account.period', string='Force Period', - domain=[('state', '!=', 'done')], copy=False, - help="Keep empty to use the period of the validation(invoice) date.", + date = fields.Date(string='Accounting Date', + copy=False, + help="Keep empty to use the invoice date.", readonly=True, states={'draft': [('readonly', False)]}) account_id = fields.Many2one('account.account', string='Account', required=True, readonly=True, states={'draft': [('readonly', False)]}, - help="The partner account used for this invoice.") - invoice_line = fields.One2many('account.invoice.line', 'invoice_id', string='Invoice Lines', + domain=[('deprecated', '=', False)], help="The partner account used for this invoice.") + invoice_line_ids = fields.One2many('account.invoice.line', 'invoice_id', string='Invoice Lines', oldname='invoice_line', readonly=True, states={'draft': [('readonly', False)]}, copy=True) - tax_line = fields.One2many('account.invoice.tax', 'invoice_id', string='Tax Lines', + tax_line_ids = fields.One2many('account.invoice.tax', 'invoice_id', string='Tax Lines', oldname='tax_line', readonly=True, states={'draft': [('readonly', False)]}, copy=True) move_id = fields.Many2one('account.move', string='Journal Entry', readonly=True, index=True, ondelete='restrict', copy=False, help="Link to the automatically generated Journal Items.") - amount_untaxed = fields.Float(string='Subtotal', digits=dp.get_precision('Account'), + amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_compute_amount', track_visibility='always') - amount_tax = fields.Float(string='Tax', digits=dp.get_precision('Account'), + amount_untaxed_signed = fields.Monetary(string='Untaxed Amount', currency_field='company_currency_id', store=True, readonly=True, compute='_compute_amount') - amount_total = fields.Float(string='Total', digits=dp.get_precision('Account'), + amount_tax = fields.Monetary(string='Tax', store=True, readonly=True, compute='_compute_amount') + amount_total = fields.Monetary(string='Total', + store=True, readonly=True, compute='_compute_amount') + amount_total_signed = fields.Monetary(string='Total', currency_field='company_currency_id', + store=True, readonly=True, compute='_compute_amount', + help="Total amount in the currency of the company, negative for credit notes.") currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=_default_currency, track_visibility='always') + company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True) journal_id = fields.Many2one('account.journal', string='Journal', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=_default_journal, - domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]") + domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale'], 'in_refund': ['purchase'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]") company_id = fields.Many2one('res.company', string='Company', change_default=True, required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['res.company']._company_default_get('account.invoice')) - check_total = fields.Float(string='Verification Total', digits=dp.get_precision('Account'), + check_total = fields.Monetary(string='Verification Total', readonly=True, states={'draft': [('readonly', False)]}, default=0.0) - reconciled = fields.Boolean(string='Paid/Reconciled', - store=True, readonly=True, compute='_compute_reconciled', + reconciled = fields.Boolean(string='Paid/Reconciled', store=True, readonly=True, compute='_compute_residual', help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment.") partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account', help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.', readonly=True, states={'draft': [('readonly', False)]}) - move_lines = fields.Many2many('account.move.line', string='Entry Lines', - compute='_compute_move_lines') - residual = fields.Float(string='Balance', digits=dp.get_precision('Account'), - compute='_compute_residual', store=True, - help="Remaining amount due.") - payment_ids = fields.Many2many('account.move.line', string='Payments', - compute='_compute_payments') - move_name = fields.Char(string='Journal Entry', readonly=True, - states={'draft': [('readonly', False)]}, copy=False) + residual = fields.Monetary(string='Amount Due', + compute='_compute_residual', store=True, help="Remaining amount due.") + residual_signed = fields.Monetary(string='Amount Due', currency_field='company_currency_id', + compute='_compute_residual', store=True, help="Remaining amount due in the currency of the company.") + payment_ids = fields.Many2many('account.payment', 'account_invoice_payment_rel', 'invoice_id', 'payment_id', string="Payments", copy=False, readonly=True) + payment_move_line_ids = fields.Many2many('account.move.line', string='Payments', compute='_compute_payments') user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user) - fiscal_position = fields.Many2one('account.fiscal.position', string='Fiscal Position', + fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position', readonly=True, states={'draft': [('readonly', False)]}) commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', related='partner_id.commercial_partner_id', store=True, readonly=True, help="The commercial entity that will be used on Journal Entries for this invoice") + outstanding_credits_debits_widget = fields.Text(compute='_get_outstanding_info_JSON') + payments_widget = fields.Text(compute='_get_payment_info_JSON') + has_outstanding = fields.Boolean(compute='_get_outstanding_info_JSON') + _sql_constraints = [ - ('number_uniq', 'unique(number, company_id, journal_id, type)', - 'Invoice Number must be unique per Company!'), + ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'), ] @api.model def create(self, vals): - return super(account_invoice, self.with_context(mail_create_nolog = True)).create(vals) + return super(AccountInvoice, self.with_context(mail_create_nolog=True)).create(vals) @api.model def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False): - context = self._context - def get_view_id(xid, name): try: return self.env['ir.model.data'].xmlid_to_res_id('account.' + xid, raise_if_not_found=True) except ValueError: - try: - return self.env['ir.ui.view'].search([('name', '=', name)], limit=1).id - except Exception: - return False # view not found + view = self.env['ir.ui.view'].search([('name', '=', name)], limit=1) + if not view: + return False + return view.id + context = self._context if context.get('active_model') == 'res.partner' and context.get('active_ids'): partner = self.env['res.partner'].browse(context['active_ids'])[0] if not view_type: @@ -332,47 +332,14 @@ class account_invoice(models.Model): view_id = get_view_id('invoice_supplier_form', 'account.invoice.supplier.form') elif partner.customer and not partner.supplier: view_id = get_view_id('invoice_form', 'account.invoice.form') - - res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - - # adapt selection of field journal_id - for field in res['fields']: - if field == 'journal_id' and type: - journal_select = self.env['account.journal']._name_search('', [('type', '=', type)], name_get_uid=1) - res['fields'][field]['selection'] = journal_select - - doc = etree.XML(res['arch']) - - if context.get('type'): - for node in doc.xpath("//field[@name='partner_bank_id']"): - if context['type'] == 'in_refund': - node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]") - elif context['type'] == 'out_refund': - node.set('domain', "[('partner_id', '=', partner_id)]") - - if view_type == 'search': - if context.get('type') in ('out_invoice', 'out_refund'): - for node in doc.xpath("//group[@name='extended filter']"): - doc.remove(node) - - if view_type == 'tree': - partner_string = _('Customer') - if context.get('type') in ('in_invoice', 'in_refund'): - partner_string = _('Supplier') - for node in doc.xpath("//field[@name='reference']"): - node.set('invisible', '0') - for node in doc.xpath("//field[@name='partner_id']"): - node.set('string', partner_string) - - res['arch'] = etree.tostring(doc) - return res + return super(AccountInvoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) @api.multi def invoice_print(self): """ Print the invoice and mark it as sent, so that we can see more easily the next step of the workflow """ - assert len(self) == 1, 'This option should only be used for a single id at a time.' + self.ensure_one() self.sent = True return self.env['report'].get_action(self, 'account.report_invoice') @@ -381,7 +348,7 @@ class account_invoice(models.Model): """ Open a window to compose an email, with the edi invoice template message loaded by default """ - assert len(self) == 1, 'This option should only be used for a single id at a time.' + self.ensure_one() template = self.env.ref('account.email_template_edi_invoice', False) compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) ctx = dict( @@ -404,6 +371,26 @@ class account_invoice(models.Model): 'context': ctx, } + @api.multi + def compute_taxes(self): + """Function used in other module to compute the taxes on a fresh invoice created (onchanges did not applied)""" + account_invoice_tax = self.env['account.invoice.tax'] + ctx = dict(self._context) + for invoice in self: + # Delete non-manual tax lines + self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,)) + self.invalidate_cache() + + # Generate one tax line per tax, however many invoice lines it's applied to + tax_grouped = invoice.get_taxes_values() + + # Create new tax lines + for tax in tax_grouped.values(): + account_invoice_tax.create(tax) + + # dummy write on self to trigger recomputations + return self.with_context(ctx).write({'invoice_line_ids': []}) + @api.multi def confirm_paid(self): return self.write({'state': 'paid'}) @@ -413,20 +400,31 @@ class account_invoice(models.Model): for invoice in self: if invoice.state not in ('draft', 'cancel'): raise UserError(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.')) - elif invoice.internal_number: + elif invoice.move_name: raise UserError(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.')) - return super(account_invoice, self).unlink() + return super(AccountInvoice, self).unlink() + @api.onchange('invoice_line_ids') @api.multi - def onchange_partner_id(self, type, partner_id, date_invoice=False, - payment_term=False, partner_bank_id=False, company_id=False): + def _onchange_invoice_line_ids(self): + taxes_grouped = self.get_taxes_values() + tax_lines = self.tax_line_ids.browse([]) + for tax in taxes_grouped.values(): + tax_lines += tax_lines.new(tax) + self.tax_line_ids = tax_lines + return + + @api.onchange('partner_id', 'company_id') + def _onchange_partner_id(self): account_id = False payment_term_id = False fiscal_position = False bank_id = False - - if partner_id: - p = self.env['res.partner'].browse(partner_id) + p = self.partner_id + company_id = self.company_id.id + type = self.type + if p: + partner_id = p.id rec_account = p.property_account_receivable pay_account = p.property_account_payable if company_id: @@ -454,135 +452,31 @@ class account_invoice(models.Model): account_id = pay_account.id payment_term_id = p.property_supplier_payment_term.id fiscal_position = p.property_account_position.id - bank_id = p.bank_ids and p.bank_ids[0].id or False - - result = {'value': { - 'account_id': account_id, - 'payment_term': payment_term_id, - 'fiscal_position': fiscal_position, - }} + bank_id = p.bank_ids and p.bank_ids.ids[0] or False + self.account_id = account_id + self.payment_term_id = payment_term_id + self.fiscal_position_id = fiscal_position if type in ('in_invoice', 'in_refund'): - result['value']['partner_bank_id'] = bank_id + self.partner_bank_id = bank_id - if payment_term != payment_term_id: - if payment_term_id: - to_update = self.onchange_payment_term_date_invoice(payment_term_id, date_invoice) - result['value'].update(to_update.get('value', {})) - else: - result['value']['date_due'] = False + @api.onchange('journal_id') + def _onchange_journal_id(self): + if self.journal_id: + self.currency_id = self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id - if partner_bank_id != bank_id: - to_update = self.onchange_partner_bank(bank_id) - result['value'].update(to_update.get('value', {})) - - return result - - @api.multi - def onchange_journal_id(self, journal_id=False): - if journal_id: - journal = self.env['account.journal'].browse(journal_id) - return { - 'value': { - 'currency_id': journal.currency.id or journal.company_id.currency_id.id, - 'company_id': journal.company_id.id, - } - } - return {} - - @api.multi - def onchange_payment_term_date_invoice(self, payment_term_id, date_invoice): + @api.onchange('payment_term_id', 'date_invoice') + def _onchange_payment_term_date_invoice(self): + date_invoice = self.date_invoice if not date_invoice: date_invoice = fields.Date.context_today(self) - if not payment_term_id: - # To make sure the invoice due date should contain due date which is - # entered by user when there is no payment term defined - return {'value': {'date_due': self.date_due or date_invoice}} - pterm = self.env['account.payment.term'].browse(payment_term_id) - pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0] - if pterm_list: - return {'value': {'date_due': max(line[0] for line in pterm_list)}} + if not self.payment_term_id: + # When no payment term defined + self.date_due = self.date_due or self.date_invoice else: - raise UserError(_('The payment term of supplier does not have a payment term line.')) - - @api.multi - def onchange_invoice_line(self, lines): - return {} - - @api.multi - def onchange_partner_bank(self, partner_bank_id=False): - return {'value': {}} - - @api.multi - def onchange_company_id(self, company_id, part_id, type, invoice_line, currency_id): - # TODO: add the missing context parameter when forward-porting in trunk - # so we can remove this hack! - self = self.with_context(self.env['res.users'].context_get()) - - values = {} - domain = {} - - if company_id and part_id and type: - p = self.env['res.partner'].browse(part_id) - if p.property_account_payable and p.property_account_receivable and \ - p.property_account_payable.company_id.id != company_id and \ - p.property_account_receivable.company_id.id != company_id: - prop = self.env['ir.property'] - rec_dom = [('name', '=', 'property_account_receivable'), ('company_id', '=', company_id)] - pay_dom = [('name', '=', 'property_account_payable'), ('company_id', '=', company_id)] - res_dom = [('res_id', '=', 'res.partner,%s' % part_id)] - rec_prop = prop.search(rec_dom + res_dom) or prop.search(rec_dom) - pay_prop = prop.search(pay_dom + res_dom) or prop.search(pay_dom) - rec_account = rec_prop.get_by_record(rec_prop) - pay_account = pay_prop.get_by_record(pay_prop) - if not rec_account and not pay_account: - action = self.env.ref('account.action_account_config') - msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.') - raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) - - if type in ('out_invoice', 'out_refund'): - acc_id = rec_account.id - else: - acc_id = pay_account.id - values= {'account_id': acc_id} - - if self: - if company_id: - for line in self.invoice_line: - if not line.account_id: - continue - if line.account_id.company_id.id == company_id: - continue - accounts = self.env['account.account'].search([('name', '=', line.account_id.name), ('company_id', '=', company_id)]) - if not accounts: - action = self.env.ref('account.action_account_config') - msg = _('Cannot find a chart of accounts for this company, You should configure it. \nPlease go to Account Configuration.') - raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) - line.write({'account_id': accounts[-1].id}) - else: - for line_cmd in invoice_line or []: - if len(line_cmd) >= 3 and isinstance(line_cmd[2], dict): - line = self.env['account.account'].browse(line_cmd[2]['account_id']) - if line.company_id.id != company_id: - raise UserError(_("""Configuration Error!\nInvoice line account's company and invoice's company does not match.""")) - - if company_id and type: - journal_type = TYPE2JOURNAL[type] - journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)]) - if journals: - values['journal_id'] = journals[0].id - journal_defaults = self.env['ir.values'].get_defaults_dict('account.invoice', 'type=%s' % type) - if 'journal_id' in journal_defaults: - values['journal_id'] = journal_defaults['journal_id'] - if not values.get('journal_id'): - field_desc = journals.fields_get(['type']) - type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type) - action = self.env.ref('account.action_account_journal_form') - msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label - raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) - domain = {'journal_id': [('id', 'in', journals.ids)]} - - return {'value': values, 'domain': domain} + pterm = self.payment_term_id + pterm_list = pterm.compute(value=1, date_ref=date_invoice)[0] + self.date_due = max(line[0] for line in pterm_list) @api.multi def action_cancel_draft(self): @@ -592,96 +486,53 @@ class account_invoice(models.Model): self.create_workflow() return True - @api.one - @api.returns('ir.ui.view') - def get_formview_id(self): - """ Update form view id of action to open the invoice """ - if self.type == 'in_invoice': - return self.env.ref('account.invoice_supplier_form') - else: - return self.env.ref('account.invoice_form') - @api.multi - def move_line_id_payment_get(self): - # return the move line ids with the same account as the invoice self - if not self.id: - return [] - query = """ SELECT l.id - FROM account_move_line l, account_invoice i - WHERE i.id = %s AND l.move_id = i.move_id AND l.account_id = i.account_id - """ - self._cr.execute(query, (self.id,)) - return [row[0] for row in self._cr.fetchall()] + def get_taxes_values(self): + tax_grouped = {} + for line in self.invoice_line_ids: + price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes'] + for tax in taxes: + val = { + 'invoice_id': self.id, + 'name': tax['name'], + 'tax_id': tax['id'], + 'amount': tax['amount'], + 'manual': False, + 'sequence': tax['sequence'], + 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, + 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id), + } - @api.multi - def test_paid(self): - # check whether all corresponding account move lines are reconciled - line_ids = self.move_line_id_payment_get() - if not line_ids: - return False - query = "SELECT reconcile_id FROM account_move_line WHERE id IN %s" - self._cr.execute(query, (tuple(line_ids),)) - return all(row[0] for row in self._cr.fetchall()) + # If the taxes generate moves on the same financial account as the invoice line, + # propagate the analytic account from the invoice line to the tax line. + # This is necessary in situations were (part of) the taxes cannot be reclaimed, + # to ensure the tax move is allocated to the proper analytic account. + if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id: + val['account_analytic_id'] = line.account_analytic_id.id - @api.multi - def button_reset_taxes(self): - account_invoice_tax = self.env['account.invoice.tax'] - ctx = dict(self._context) - for invoice in self: - self._cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (invoice.id,)) - self.invalidate_cache() - partner = invoice.partner_id - if partner.lang: - ctx['lang'] = partner.lang - for taxe in account_invoice_tax.compute(invoice.with_context(ctx)).values(): - account_invoice_tax.create(taxe) - # dummy write on self to trigger recomputations - return self.with_context(ctx).write({'invoice_line': []}) - - @api.multi - def button_compute(self, set_total=False): - self.button_reset_taxes() - for invoice in self: - if set_total: - invoice.check_total = invoice.amount_total - return True - - @api.multi - def _get_analytic_lines(self): - """ Return a list of dict for creating analytic lines for self[0] """ - company_currency = self.company_id.currency_id - sign = 1 if self.type in ('out_invoice', 'in_refund') else -1 - - iml = self.env['account.invoice.line'].move_line_get(self.id) - for il in iml: - if il['account_analytic_id']: - if self.type in ('in_invoice', 'in_refund'): - ref = self.reference + key = tax['id'] + if key not in tax_grouped: + tax_grouped[key] = val else: - ref = self.number - if not self.journal_id.analytic_journal_id: - raise UserError(_("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,)) - currency = self.currency_id.with_context(date=self.date_invoice) - il['analytic_lines'] = [(0,0, { - 'name': il['name'], - 'date': self.date_invoice, - 'account_id': il['account_analytic_id'], - 'unit_amount': il['quantity'], - 'amount': currency.compute(il['price'], company_currency) * sign, - 'product_id': il['product_id'], - 'product_uom_id': il['uos_id'], - 'general_account_id': il['account_id'], - 'journal_id': self.journal_id.analytic_journal_id.id, - 'ref': ref, - })] - return iml + tax_grouped[key]['amount'] += val['amount'] + return tax_grouped + + @api.one + def register_payment(self, payment_line, writeoff_acc_id=False, writeoff_journal_id=False): + """ Reconcile payable/receivable lines from the invoice with payment_line """ + line_to_reconcile = self.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable')) + return (line_to_reconcile + payment_line).reconcile(writeoff_acc_id, writeoff_journal_id) + + @api.v7 + def assign_outstanding_credit(self, cr, uid, id, payment_id, context=None): + return self.browse(cr, uid, id, context).register_payment(self.pool.get('account.move.line').browse(cr, uid, payment_id, context)) @api.multi def action_date_assign(self): for inv in self: - res = inv.onchange_payment_term_date_invoice(inv.payment_term.id, inv.date_invoice) - if res and res.get('value'): - inv.write(res['value']) + # Here the onchange will automatically write to the database + inv._onchange_payment_term_date_invoice() return True @api.multi @@ -697,31 +548,7 @@ class account_invoice(models.Model): return move_lines @api.multi - def check_tax_lines(self, compute_taxes): - account_invoice_tax = self.env['account.invoice.tax'] - company_currency = self.company_id.currency_id - if not self.tax_line: - for tax in compute_taxes.values(): - account_invoice_tax.create(tax) - else: - tax_key = [] - precision = self.env['decimal.precision'].precision_get('Account') - for tax in self.tax_line: - if tax.manual: - continue - key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id) - tax_key.append(key) - if key not in compute_taxes: - raise UserError(_('Global taxes defined, but they are not in invoice lines !')) - base = compute_taxes[key]['base'] - if float_compare(abs(base - tax.base), company_currency.rounding, precision_digits=precision) == 1: - raise UserError(_('Tax base different!\nClick on compute to update the tax base.')) - for key in compute_taxes: - if key not in tax_key: - raise UserError(_('Taxes are missing!\nClick on compute button.')) - - @api.multi - def compute_invoice_totals(self, company_currency, ref, invoice_move_lines): + def compute_invoice_totals(self, company_currency, invoice_move_lines): total = 0 total_currency = 0 for line in invoice_move_lines: @@ -733,8 +560,7 @@ class account_invoice(models.Model): else: line['currency_id'] = False line['amount_currency'] = False - line['ref'] = ref - if self.type in ('out_invoice','in_refund'): + if self.type in ('out_invoice', 'in_refund'): total += line['price'] total_currency += line['amount_currency'] or line['price'] line['price'] = - line['price'] @@ -743,6 +569,52 @@ class account_invoice(models.Model): total_currency -= line['amount_currency'] or line['price'] return total, total_currency, invoice_move_lines + @api.model + def invoice_line_move_line_get(self): + res = [] + for line in self.invoice_line_ids: + tax_ids = [] + for tax in line.invoice_line_tax_ids: + tax_ids.append((4, tax.id, None)) + for child in tax.children_tax_ids: + if child.type_tax_use != 'none': + tax_ids.append((4, child.id, None)) + + move_line_dict = { + 'invl_id': line.id, + 'type': 'src', + 'name': line.name.split('\n')[0][:64], + 'price_unit': line.price_unit, + 'quantity': line.quantity, + 'price': line.price_subtotal, + 'account_id': line.account_id.id, + 'product_id': line.product_id.id, + 'uos_id': line.uos_id.id, + 'account_analytic_id': line.account_analytic_id.id, + 'tax_ids': tax_ids, + 'invoice': self.id, + } + if line['account_analytic_id']: + move_line_dict['analytic_lines'] = [(0, 0, line._get_analytic_line())] + res.append(move_line_dict) + return res + + @api.model + def tax_line_move_line_get(self): + res = [] + for tax_line in self.tax_line_ids: + res.append({ + 'tax_line_id': tax_line.tax_id.id, + 'type': 'tax', + 'name': tax_line.name, + 'price_unit': tax_line.amount, + 'quantity': 1, + 'price': tax_line.amount, + 'account_id': tax_line.account_id.id, + 'account_analytic_id': tax_line.account_analytic_id.id, + }) + return res + def inv_line_characteristic_hashcode(self, invoice_line): """Overridable hashcode generation for invoice lines. Lines having the same hashcode will be grouped together if the journal has the 'group line' option. Of course a module @@ -750,7 +622,7 @@ class account_invoice(models.Model): or not.""" return "%s-%s-%s-%s-%s" % ( invoice_line['account_id'], - invoice_line.get('tax_code_id', 'False'), + invoice_line.get('tax_line_id', 'False'), invoice_line.get('product_id', 'False'), invoice_line.get('analytic_account_id', 'False'), invoice_line.get('date_maturity', 'False'), @@ -766,25 +638,23 @@ class account_invoice(models.Model): am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit']) line2[tmp]['debit'] = (am > 0) and am or 0.0 line2[tmp]['credit'] = (am < 0) and -am or 0.0 - line2[tmp]['tax_amount'] += l['tax_amount'] line2[tmp]['analytic_lines'] += l['analytic_lines'] else: line2[tmp] = l line = [] for key, val in line2.items(): - line.append((0,0,val)) + line.append((0, 0, val)) return line @api.multi def action_move_create(self): """ Creates invoice related analytics and financial move lines """ - account_invoice_tax = self.env['account.invoice.tax'] account_move = self.env['account.move'] for inv in self: if not inv.journal_id.sequence_id: raise UserError(_('Please define sequence on the journal related to this invoice.')) - if not inv.invoice_line: + if not inv.invoice_line_ids: raise UserError(_('Please create some invoice lines.')) if inv.move_id: continue @@ -794,47 +664,24 @@ class account_invoice(models.Model): if not inv.date_invoice: inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)}) date_invoice = inv.date_invoice - company_currency = inv.company_id.currency_id - # create the analytical lines, one move line per invoice line - iml = inv._get_analytic_lines() - # check if taxes are all computed - compute_taxes = account_invoice_tax.compute(inv.with_context(lang=inv.partner_id.lang)) - inv.check_tax_lines(compute_taxes) + + # create move lines (one per invoice line + eventual taxes and analytic lines) + iml = inv.invoice_line_move_line_get() + iml += inv.tax_line_move_line_get() # I disabled the check_total feature if self.env['res.users'].has_group('account.group_supplier_inv_check_total'): if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding / 2.0): raise UserError(_('Please verify the price of the invoice!\nThe encoded total does not match the computed total.')) - if inv.payment_term: - total_fixed = total_percent = 0 - for line in inv.payment_term.line_ids: - if line.value == 'fixed': - total_fixed += line.value_amount - if line.value == 'procent': - total_percent += (line.value_amount/100.0) - total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0) - if (total_fixed + total_percent) > 100: - raise UserError(_("Cannot create the invoice.\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. In order to avoid rounding issues, the latest line of your payment term must be of type 'balance'.")) - - # one move line per tax line - iml += account_invoice_tax.move_line_get(inv.id) - - if inv.type in ('in_invoice', 'in_refund'): - ref = inv.reference - else: - ref = inv.number - diff_currency = inv.currency_id != company_currency # create one move line for the total and possibly adjust the other lines amount - total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, ref, iml) + total, total_currency, iml = inv.with_context(ctx).compute_invoice_totals(company_currency, iml) - name = inv.supplier_invoice_number or inv.name or '/' - totlines = [] - if inv.payment_term: - totlines = inv.with_context(ctx).payment_term.compute(total, date_invoice)[0] - if totlines: + name = inv.name or '/' + if inv.payment_term_id: + totlines = inv.with_context(ctx).payment_term_id.compute(total, date_invoice)[0] res_amount_currency = total_currency ctx['date'] = date_invoice for i, t in enumerate(totlines): @@ -856,7 +703,7 @@ class account_invoice(models.Model): 'date_maturity': t[0], 'amount_currency': diff_currency and amount_currency, 'currency_id': diff_currency and inv.currency_id.id, - 'ref': ref, + 'invoice': inv.id }) else: iml.append({ @@ -867,52 +714,37 @@ class account_invoice(models.Model): 'date_maturity': inv.date_due, 'amount_currency': diff_currency and total_currency, 'currency_id': diff_currency and inv.currency_id.id, - 'ref': ref + 'invoice': inv.id }) - - date = date_invoice - part = self.env['res.partner']._find_accounting_partner(inv.partner_id) - - line = [(0, 0, self.line_get_convert(l, part.id, date)) for l in iml] + line = [(0, 0, self.line_get_convert(l, part.id)) for l in iml] line = inv.group_lines(iml, line) journal = inv.journal_id.with_context(ctx) - if journal.centralisation: - raise UserError(_('You cannot create an invoice on a centralized journal. Uncheck the centralized counterpart box in the related journal from the configuration menu.')) - line = inv.finalize_invoice_move_lines(line) + date = inv.date or date_invoice move_vals = { - 'ref': inv.reference or inv.name, - 'line_id': line, + 'ref': inv.reference, + 'line_ids': line, 'journal_id': journal.id, - 'date': inv.date_invoice, + 'date': date, 'narration': inv.comment, - 'company_id': inv.company_id.id, } ctx['company_id'] = inv.company_id.id - period = inv.period_id - if not period: - period = period.with_context(ctx).find(date_invoice)[:1] - if period: - move_vals['period_id'] = period.id - for i in line: - i[2]['period_id'] = period.id - - ctx['invoice'] = inv + ctx['dont_create_taxes'] = True + ctx['invoice'] = inv.move_name and inv or False move = account_move.with_context(ctx).create(move_vals) # make the invoice point to that move vals = { 'move_id': move.id, - 'period_id': period.id, + 'date': date, 'move_name': move.name, } inv.with_context(ctx).write(vals) # Pass invoice in context in method post: used if you want to get the same # account move reference when creating the same invoice after a cancelled one: move.post() - self._log_event() return True @api.multi @@ -920,68 +752,34 @@ class account_invoice(models.Model): return self.write({'state': 'open'}) @api.model - def line_get_convert(self, line, part, date): + def line_get_convert(self, line, part): return { 'date_maturity': line.get('date_maturity', False), 'partner_id': part, 'name': line['name'][:64], - 'date': date, - 'debit': line['price']>0 and line['price'], - 'credit': line['price']<0 and -line['price'], + 'debit': line['price'] > 0 and line['price'], + 'credit': line['price'] < 0 and -line['price'], 'account_id': line['account_id'], 'analytic_lines': line.get('analytic_lines', []), - 'amount_currency': line['price']>0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), + 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), 'currency_id': line.get('currency_id', False), - 'tax_code_id': line.get('tax_code_id', False), - 'tax_amount': line.get('tax_amount', False), - 'ref': line.get('ref', False), - 'quantity': line.get('quantity',1.00), + 'quantity': line.get('quantity', 1.00), 'product_id': line.get('product_id', False), 'product_uom_id': line.get('uos_id', False), 'analytic_account_id': line.get('account_analytic_id', False), + 'invoice': line.get('invoice', False), + 'tax_ids': line.get('tax_ids', False), + 'tax_line_id': line.get('tax_line_id', False), } - @api.multi - def action_number(self): - #TODO: not correct fix but required a fresh values before reading it. - self.write({}) - - for inv in self: - self.write({'internal_number': inv.number}) - - if inv.type in ('in_invoice', 'in_refund'): - if not inv.reference: - ref = inv.number - else: - ref = inv.reference - else: - ref = inv.number - - self._cr.execute(""" UPDATE account_move SET ref=%s - WHERE id=%s AND (ref IS NULL OR ref = '')""", - (ref, inv.move_id.id)) - self._cr.execute(""" UPDATE account_move_line SET ref=%s - WHERE move_id=%s AND (ref IS NULL OR ref = '')""", - (ref, inv.move_id.id)) - self._cr.execute(""" UPDATE account_analytic_line SET ref=%s - FROM account_move_line - WHERE account_move_line.move_id = %s AND - account_analytic_line.move_id = account_move_line.id""", - (ref, inv.move_id.id)) - self.invalidate_cache() - - return True - @api.multi def action_cancel(self): moves = self.env['account.move'] for inv in self: if inv.move_id: moves += inv.move_id - if inv.payment_ids: - for move_line in inv.payment_ids: - if move_line.reconcile_partial_id.line_partial_ids: - raise UserError(_('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.')) + if inv.payment_move_line_ids: + raise UserError(_('You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first.')) # First, set the invoices as cancelled and detach the move ids self.write({'state': 'cancel', 'move_id': False}) @@ -992,21 +790,15 @@ class account_invoice(models.Model): # Note that the corresponding move_lines and move_reconciles # will be automatically deleted too moves.unlink() - self._log_event(-1.0, 'Cancel Invoice') return True ################### - @api.multi - def _log_event(self, factor=1.0, name='Open Invoice'): - #TODO: implement messages system - return True - @api.multi def name_get(self): TYPES = { 'out_invoice': _('Invoice'), - 'in_invoice': _('Supplier Invoice'), + 'in_invoice': _('Supplier Bill'), 'out_refund': _('Refund'), 'in_refund': _('Supplier Refund'), } @@ -1042,159 +834,110 @@ class account_invoice(models.Model): values[name] = line[name].id elif field.type not in ['many2many', 'one2many']: values[name] = line[name] - elif name == 'invoice_line_tax_id': + elif name == 'invoice_line_tax_ids': values[name] = [(6, 0, line[name].ids)] result.append((0, 0, values)) return result @api.model - def _prepare_refund(self, invoice, date=None, period_id=None, description=None, journal_id=None): + def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None): """ Prepare the dict of values to create the new refund from the invoice. This method may be overridden to implement custom refund generation (making sure to call super() to establish a clean extension chain). :param record invoice: invoice to refund - :param string date: refund creation date from the wizard - :param integer period_id: force account.period from the wizard + :param string date_invoice: refund creation date from the wizard + :param integer date: force date from the wizard :param string description: description of the refund from the wizard :param integer journal_id: account.journal from the wizard :return: dict of value to create() the refund """ values = {} for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id', - 'account_id', 'currency_id', 'payment_term', 'user_id', 'fiscal_position']: + 'account_id', 'currency_id', 'payment_term_id', 'user_id', 'fiscal_position_id']: if invoice._fields[field].type == 'many2one': values[field] = invoice[field].id else: values[field] = invoice[field] or False - values['invoice_line'] = self._refund_cleanup_lines(invoice.invoice_line) + values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids) - tax_lines = filter(lambda l: l.manual, invoice.tax_line) - values['tax_line'] = self._refund_cleanup_lines(tax_lines) + tax_lines = filter(lambda l: l.manual, invoice.tax_line_ids) + values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines) if journal_id: journal = self.env['account.journal'].browse(journal_id) elif invoice['type'] == 'in_invoice': - journal = self.env['account.journal'].search([('type', '=', 'purchase_refund')], limit=1) + journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1) else: - journal = self.env['account.journal'].search([('type', '=', 'sale_refund')], limit=1) + journal = self.env['account.journal'].search([('type', '=', 'sale')], limit=1) values['journal_id'] = journal.id values['type'] = TYPE2REFUND[invoice['type']] - values['date_invoice'] = date or fields.Date.context_today(invoice) + values['date_invoice'] = date_invoice or fields.Date.context_today(invoice) values['state'] = 'draft' values['number'] = False values['origin'] = invoice.number - if period_id: - values['period_id'] = period_id + if date: + values['date'] = date if description: values['name'] = description return values @api.multi @api.returns('self') - def refund(self, date=None, period_id=None, description=None, journal_id=None): + def refund(self, date_invoice=None, date=None, description=None, journal_id=None): new_invoices = self.browse() for invoice in self: # create the new invoice - values = self._prepare_refund(invoice, date=date, period_id=period_id, + values = self._prepare_refund(invoice, date_invoice=date_invoice, date=date, description=description, journal_id=journal_id) new_invoices += self.create(values) return new_invoices @api.v8 - def pay_and_reconcile(self, pay_amount, pay_account_id, period_id, pay_journal_id, - writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=''): - # TODO check if we can use different period for payment and the writeoff line - assert len(self)==1, "Can only pay one invoice at a time." - # Take the seq as name for move - SIGN = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1} - direction = SIGN[self.type] - # take the chosen date - date = self._context.get('date_p') or fields.Date.context_today(self) + def pay_and_reconcile(self, pay_journal, pay_amount=None, date=None, writeoff_acc=None): + """ Create and post an account.payment for the invoice self, which creates a journal entry that reconciles the invoice. - # Take the amount in currency and the currency of the payment - if self._context.get('amount_currency') and self._context.get('currency_id'): - amount_currency = self._context['amount_currency'] - currency_id = self._context['currency_id'] + :param pay_journal: journal in which the payment entry will be created + :param pay_amount: amount of the payment to register, defaults to the residual of the invoice + :param date: payment date, defaults to fields.Date.context_today(self) + :param writeoff_acc: account in which to create a writeoff if pay_amount < self.residual, so that the invoice is fully paid + """ + assert len(self) == 1, "Can only pay one invoice at a time." + payment_type = self.type in ('out_invoice', 'in_refund') and 'inbound' or 'outbound' + if payment_type == 'inbound': + payment_method = self.env.ref('account.account_payment_method_manual_in') + journal_payment_methods = pay_journal.inbound_payment_methods else: - amount_currency = False - currency_id = False + payment_method = self.env.ref('account.account_payment_method_manual_out') + journal_payment_methods = pay_journal.outbound_payment_methods + if payment_method not in journal_payment_methods: + raise UserError(_('No appropriate payment method enabled on journal %s') % pay_journal.name) - pay_journal = self.env['account.journal'].browse(pay_journal_id) - if self.type in ('in_invoice', 'in_refund'): - ref = self.reference - else: - ref = self.number - partner = self.partner_id._find_accounting_partner(self.partner_id) - name = name or self.invoice_line[0].name or self.number - # Pay attention to the sign for both debit/credit AND amount_currency - l1 = { - 'name': name, - 'debit': direction * pay_amount > 0 and direction * pay_amount, - 'credit': direction * pay_amount < 0 and -direction * pay_amount, - 'account_id': self.account_id.id, - 'partner_id': partner.id, - 'ref': ref, - 'date': date, - 'currency_id': currency_id, - 'amount_currency': direction * (amount_currency or 0.0), - 'company_id': self.company_id.id, - } - l2 = { - 'name': name, - 'debit': direction * pay_amount < 0 and -direction * pay_amount, - 'credit': direction * pay_amount > 0 and direction * pay_amount, - 'account_id': pay_account_id, - 'partner_id': partner.id, - 'ref': ref, - 'date': date, - 'currency_id': currency_id, - 'amount_currency': -direction * (amount_currency or 0.0), - 'company_id': self.company_id.id, - } - move = self.env['account.move'].create({ - 'ref': ref, - 'line_id': [(0, 0, l1), (0, 0, l2)], - 'journal_id': pay_journal_id, - 'period_id': period_id, - 'date': date, + payment = self.env['account.payment'].create({ + 'invoice_ids': [(6, 0, self.ids)], + 'amount': pay_amount or self.residual, + 'payment_date': date or fields.Date.context_today(self), + 'communication': self.type in ('in_invoice', 'in_refund') and self.reference or self.number, + 'partner_id': self.partner_id.id, + 'partner_type': self.type in ('out_invoice', 'out_refund') and 'customer' or 'supplier', + 'journal_id': pay_journal.id, + 'payment_type': payment_type, + 'payment_method': payment_method.id, + 'payment_difference_handling': writeoff_acc and 'reconcile' or 'open', + 'writeoff_account': writeoff_acc and writeoff_acc.id or False, }) - - move_ids = (move | self.move_id).ids - self._cr.execute("SELECT id FROM account_move_line WHERE move_id IN %s", - (tuple(move_ids),)) - lines = self.env['account.move.line'].browse([r[0] for r in self._cr.fetchall()]) - lines2rec = lines.browse() - total = 0.0 - for line in itertools.chain(lines, self.payment_ids): - if line.account_id == self.account_id: - lines2rec += line - total += (line.debit or 0.0) - (line.credit or 0.0) - - inv_id, name = self.name_get()[0] - if not round(total, self.env['decimal.precision'].precision_get('Account')) or writeoff_acc_id: - lines2rec.reconcile('manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id) - else: - code = self.currency_id.symbol - # TODO: use currency's formatting function - msg = _("Invoice partially paid: %s%s of %s%s (%s%s remaining).") % \ - (pay_amount, code, self.amount_total, code, total, code) - self.message_post(body=msg) - lines2rec.reconcile_partial('manual') - - # Update the stored value (fields.function), so we write to trigger recompute - return self.write({}) + payment.post() @api.v7 - def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id, - writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''): + def pay_and_reconcile(self, cr, uid, ids, pay_journal_id, pay_amount=None, date=None, writeoff_acc_id=None, context=None): recs = self.browse(cr, uid, ids, context) - return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id, - writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name) + pay_journal = self.pool.get('account.journal').browse(cr, uid, pay_journal_id, context=context) + writeoff_acc = self.pool.get('account.account').browse(cr, uid, writeoff_acc_id, context=context) + return recs.pay_and_reconcile(pay_journal, pay_amount, date, writeoff_acc) @api.multi def _track_subtype(self, init_values): @@ -1205,71 +948,78 @@ class account_invoice(models.Model): return 'account.mt_invoice_validated' elif 'state' in init_values and self.state == 'draft' and self.type in ('out_invoice', 'out_refund'): return 'account.mt_invoice_created' - return super(account_invoice, self)._track_subtype(init_values) + return super(AccountInvoice, self)._track_subtype(init_values) -class account_invoice_line(models.Model): +class AccountInvoiceLine(models.Model): _name = "account.invoice.line" _description = "Invoice Line" _order = "invoice_id,sequence,id" + @api.multi + def _get_analytic_line(self): + ref = self.invoice_id.number + if not self.invoice_id.journal_id.analytic_journal_id: + raise UserError(_("No Analytic Journal! You have to define an analytic journal on the '%s' journal!") % (self.invoice_id.journal_id.name,)) + return { + 'name': self.name, + 'date': self.invoice_id.date_invoice, + 'account_id': self.account_analytic_id.id, + 'unit_amount': self.quantity, + 'amount': self.price_subtotal_signed, + 'product_id': self.product_id.id, + 'product_uom_id': self.uos_id.id, + 'general_account_id': self.account_id.id, + 'journal_id': self.invoice_id.journal_id.analytic_journal_id.id, + 'ref': ref, + } + @api.one - @api.depends('price_unit', 'discount', 'invoice_line_tax_id', 'quantity', 'product_id', - 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id') + @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity', + 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id') def _compute_price(self): + currency = self.invoice_id and self.invoice_id.currency_id or None price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) - taxes = self.invoice_line_tax_id.compute_all(price, self.quantity, - product=self.product_id, - partner=self.invoice_id.partner_id) - self.price_subtotal = price_subtotal_signed = taxes['total'] - - if not self.invoice_id: - self.price_subtotal_signed = price_subtotal_signed - else: - inv_cur = self.invoice_id.currency_id - comp_cur = self.invoice_id.company_id.currency_id - - if inv_cur != comp_cur: - inv_cur = inv_cur.with_context(date=self.invoice_id.date_invoice) - price_subtotal_signed = inv_cur.compute(price_subtotal_signed, comp_cur) - - sign = -1 if self.invoice_id.type in ['in_invoice', 'out_refund'] else 1 - self.price_subtotal = inv_cur.round(self.price_subtotal) - self.price_subtotal_signed = inv_cur.round(self.price_subtotal_signed * sign) + taxes = False + if self.invoice_line_tax_ids: + taxes = self.invoice_line_tax_ids.compute_all(price, currency, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id) + self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price + if self.invoice_id.currency_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id: + price_subtotal_signed = self.invoice_id.currency_id.compute(price_subtotal_signed, self.invoice_id.company_id.currency_id) + sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1 + self.price_subtotal_signed = price_subtotal_signed * sign @api.model def _default_price_unit(self): if not self._context.get('check_total'): return 0 + currency = self.invoice_id and self.invoice_id.currency_id or None total = self._context['check_total'] - for l in self._context.get('invoice_line', []): + for l in self._context.get('invoice_line_ids', []): if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]: vals = l[2] price = vals.get('price_unit', 0) * (1 - vals.get('discount', 0) / 100.0) total = total - (price * vals.get('quantity')) - taxes = vals.get('invoice_line_tax_id') + taxes = vals.get('invoice_line_tax_ids') if taxes and len(taxes[0]) >= 3 and taxes[0][2]: taxes = self.env['account.tax'].browse(taxes[0][2]) - tax_res = taxes.compute_all(price, vals.get('quantity'), - product=vals.get('product_id'), partner=self._context.get('partner_id')) + tax_res = taxes.compute_all(price, currency, vals.get('quantity'), vals.get('product_id'), self._context.get('partner_id')) for tax in tax_res['taxes']: total = total - tax['amount'] return total @api.model def _default_account(self): - # XXX this gets the default account for the user's company, - # it should get the default account for the invoice's company - # however, the invoice's company does not reach this point - if self._context.get('type') in ('out_invoice', 'out_refund'): - return self.env['ir.property'].get('property_account_income_categ', 'product.category') - else: - return self.env['ir.property'].get('property_account_expense_categ', 'product.category') + if self._context.get('journal_id'): + journal = self.env['account.journal'].browse(self._context.get('journal_id')) + if self._context.get('type') in ('out_invoice', 'in_refund'): + return journal.default_credit_account_id.id + return journal.default_debit_account_id.id name = fields.Text(string='Description', required=True) origin = fields.Char(string='Source Document', help="Reference of the document that produced this invoice.") - sequence = fields.Integer(string='Sequence', default=10, + sequence = fields.Integer(default=10, help="Gives the sequence of this line when displaying the invoice.") invoice_id = fields.Many2one('account.invoice', string='Invoice Reference', ondelete='cascade', index=True) @@ -1278,34 +1028,35 @@ class account_invoice_line(models.Model): product_id = fields.Many2one('product.product', string='Product', ondelete='restrict', index=True) account_id = fields.Many2one('account.account', string='Account', - required=True, domain=[('type', 'not in', ['view', 'closed'])], + required=True, domain=[('deprecated', '=', False)], default=_default_account, help="The income or expense account related to the selected product.") - price_unit = fields.Float(string='Unit Price', required=True, - digits= dp.get_precision('Product Price'), + price_unit = fields.Monetary(string='Unit Price', required=True, default=_default_price_unit) - price_subtotal = fields.Float(string='Amount', digits= dp.get_precision('Account'), + price_subtotal = fields.Monetary(string='Amount', store=True, readonly=True, compute='_compute_price') - price_subtotal_signed = fields.Float(string='Amount Signed', digits=0, + price_subtotal_signed = fields.Monetary(string='Amount Signed', currency_field='company_currency_id', store=True, readonly=True, compute='_compute_price', help="Total amount in the currency of the company, negative for credit notes.") - quantity = fields.Float(string='Quantity', digits= dp.get_precision('Product Unit of Measure'), + quantity = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1) - discount = fields.Float(string='Discount (%)', digits= dp.get_precision('Discount'), + discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) - invoice_line_tax_id = fields.Many2many('account.tax', + invoice_line_tax_ids = fields.Many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', - string='Taxes', domain=[('parent_id', '=', False)]) + string='Taxes', domain=[('type_tax_use','!=','none')], oldname='invoice_line_tax_id') account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') company_id = fields.Many2one('res.company', string='Company', related='invoice_id.company_id', store=True, readonly=True) partner_id = fields.Many2one('res.partner', string='Partner', related='invoice_id.partner_id', store=True, readonly=True) + currency_id = fields.Many2one('res.currency', related='invoice_id.currency_id', store=True) + company_currency_id = fields.Many2one('res.currency', related='invoice_id.company_currency_id', readonly=True) @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): - res = super(account_invoice_line, self).fields_view_get( + res = super(AccountInvoiceLine, self).fields_view_get( view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) if self._context.get('type'): doc = etree.XML(res['arch']) @@ -1317,335 +1068,198 @@ class account_invoice_line(models.Model): res['arch'] = etree.tostring(doc) return res - @api.multi - def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice', - partner_id=False, fposition_id=False, price_unit=False, currency_id=False, - company_id=None, date_invoice=None): - context = self._context - company_id = company_id if company_id is not None else context.get('company_id', False) - self = self.with_context(company_id=company_id, force_company=company_id) + @api.v8 + def get_invoice_line_account(self, type, product, fpos, company): + accounts = product.product_tmpl_id.get_product_accounts(fpos) + if type in ('out_invoice', 'out_refund'): + return accounts['income'] + return accounts['expense'] - if not partner_id: - raise UserError(_("You must first select a partner!")) - if not product: - if type in ('in_invoice', 'in_refund'): - return {'value': {}, 'domain': {'product_uom': []}} + def _set_taxes(self): + """ Used in on_change to set taxes """ + if self.invoice_id.type in ('out_invoice', 'out_refund'): + taxes = self.product_id.taxes_id or self.account_id.tax_ids + else: + taxes = self.product_id.supplier_taxes_id or self.account_id.tax_ids + self.invoice_line_tax_ids = self.invoice_id.fiscal_position_id.map_tax(taxes) + + @api.onchange('product_id') + def _onchange_product_id(self): + domain = {} + if not self.invoice_id: + return + + part = self.invoice_id.partner_id + fpos = self.invoice_id.fiscal_position_id + company = self.company_id + currency = self.invoice_id.currency_id + type = self.invoice_id.type + + if not part: + warning = { + 'title': _('Warning!'), + 'message': _('You must first select a partner!'), + } + return {'warning': warning} + + if not self.product_id: + if type not in ('in_invoice', 'in_refund'): + self.price_unit = 0.0 + domain['uos_id'] = [] + else: + if part.lang: + product = self.product_id.with_context(lang=part.lang) else: - return {'value': {'price_unit': 0.0}, 'domain': {'product_uom': []}} + product = self.product_id - values = {} + self.name = product.partner_ref + account = self.get_invoice_line_account(type, product, fpos, company) + self.account_id = account.id + self._set_taxes() - part = self.env['res.partner'].browse(partner_id) - fpos = self.env['account.fiscal.position'].browse(fposition_id) + if type in ('in_invoice', 'in_refund'): + self.price_unit = self.price_unit or product.standard_price + if product.description_purchase: + self.name += '\n' + product.description_purchase + else: + self.price_unit = product.lst_price + if product.description_sale: + self.name += '\n' + product.description_sale - if part.lang: - self = self.with_context(lang=part.lang) - product = self.env['product.product'].browse(product) + if not self.uos_id or product.uom_id.category_id.id != self.uos_id.category_id.id: + self.uos_id = product.uom_id.id + domain['uos_id'] = [('category_id', '=', product.uom_id.category_id.id)] - values['name'] = product.partner_ref - if type in ('out_invoice', 'out_refund'): - account = product.property_account_income or product.categ_id.property_account_income_categ + if company and currency: + if company.currency_id != currency: + if type in ('in_invoice', 'in_refund'): + self.price_unit = product.standard_price + self.price_unit = self.price_unit * currency.with_context(dict(self._context or {}, date=self.date_invoice)).rate + + if self.uos_id and self.uos_id.id != product.uom_id.id: + self.price_unit = self.env['product.uom']._compute_price( + product.uom_id.id, self.price_unit, self.uos_id.id) + return {'domain': domain} + + @api.onchange('account_id') + def _onchange_account_id(self): + if not self.account_id: + return + if not self.product_id: + fpos = self.invoice_id.fiscal_position_id + self.invoice_line_tax_ids = fpos.map_tax(self.account_id.tax_ids).ids else: - account = product.property_account_expense or product.categ_id.property_account_expense_categ - account = fpos.map_account(account) - if account: - values['account_id'] = account.id + self._set_taxes() - if type in ('out_invoice', 'out_refund'): - taxes = product.taxes_id or account.tax_ids - if product.description_sale: - values['name'] += '\n' + product.description_sale - else: - taxes = product.supplier_taxes_id or account.tax_ids - if product.description_purchase: - values['name'] += '\n' + product.description_purchase - - taxes = fpos.map_tax(taxes) - values['invoice_line_tax_id'] = taxes.ids - - if type in ('in_invoice', 'in_refund'): - values['price_unit'] = price_unit or product.standard_price - else: - values['price_unit'] = product.lst_price - - values['uos_id'] = uom_id or product.uom_id.id - domain = {'uos_id': [('category_id', '=', product.uom_id.category_id.id)]} - - company = self.env['res.company'].browse(company_id) - currency = self.env['res.currency'].browse(currency_id) - - if company and currency: - if company.currency_id != currency: - if type in ('in_invoice', 'in_refund'): - values['price_unit'] = product.standard_price - values['price_unit'] = values['price_unit'] * currency.with_context(dict(context or {}, date=date_invoice)).rate - - if values['uos_id'] and values['uos_id'] != product.uom_id.id: - values['price_unit'] = self.env['product.uom']._compute_price( - product.uom_id.id, values['price_unit'], values['uos_id']) - - return {'value': values, 'domain': domain} - - @api.multi - def uos_id_change(self, product, uom, qty=0, name='', type='out_invoice', partner_id=False, - fposition_id=False, price_unit=False, currency_id=False, company_id=None, date_invoice=None): - context = self._context - company_id = company_id if company_id != None else context.get('company_id', False) - self = self.with_context(company_id=company_id) - - result = self.product_id_change( - product, uom, qty, name, type, partner_id, fposition_id, price_unit, - currency_id, company_id=company_id, date_invoice=date_invoice - ) + @api.onchange('uos_id') + def _onchange_uos_id(self): warning = {} - if not uom: - result['value']['price_unit'] = 0.0 - if product and uom: - prod = self.env['product.product'].browse(product) - prod_uom = self.env['product.uom'].browse(uom) - if prod.uom_id.category_id != prod_uom.category_id: + result = {} + self._onchange_product_id() + if not self.uos_id: + self.price_unit = 0.0 + if self.product_id and self.uos_id: + if self.product_id.uom_id.category_id.id != self.uos_id.category_id.id: warning = { 'title': _('Warning!'), 'message': _('The selected unit of measure is not compatible with the unit of measure of the product.'), } - result['value']['uos_id'] = prod.uom_id.id + self.uos_id = self.product_id.uom_id.id if warning: result['warning'] = warning return result - @api.model - def move_line_get(self, invoice_id): - inv = self.env['account.invoice'].browse(invoice_id) - currency = inv.currency_id.with_context(date=inv.date_invoice) - company_currency = inv.company_id.currency_id - res = [] - for line in inv.invoice_line: - mres = self.move_line_get_item(line) - mres['invl_id'] = line.id - res.append(mres) - tax_code_found = False - taxes = line.invoice_line_tax_id.compute_all( - (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0)), - line.quantity, line.product_id, inv.partner_id)['taxes'] - for tax in taxes: - if inv.type in ('out_invoice', 'in_invoice'): - tax_code_id = tax['base_code_id'] - tax_amount = line.price_subtotal * tax['base_sign'] - else: - tax_code_id = tax['ref_base_code_id'] - tax_amount = line.price_subtotal * tax['ref_base_sign'] - - if tax_code_found: - if not tax_code_id: - continue - res.append(dict(mres)) - res[-1]['price'] = 0.0 - res[-1]['account_analytic_id'] = False - elif not tax_code_id: - continue - tax_code_found = True - - res[-1]['tax_code_id'] = tax_code_id - res[-1]['tax_amount'] = currency.compute(tax_amount, company_currency) - - return res - - @api.model - def move_line_get_item(self, line): - return { - 'type': 'src', - 'name': line.name.split('\n')[0][:64], - 'price_unit': line.price_unit, - 'quantity': line.quantity, - 'price': line.price_subtotal, - 'account_id': line.account_id.id, - 'product_id': line.product_id.id, - 'uos_id': line.uos_id.id, - 'account_analytic_id': line.account_analytic_id.id, - 'taxes': line.invoice_line_tax_id, - } - - # - # Set the tax field according to the account and the fiscal position - # - @api.multi - def onchange_account_id(self, product_id, partner_id, inv_type, fposition_id, account_id, date_invoice=None): - if not account_id: - return {} - unique_tax_ids = [] - account = self.env['account.account'].browse(account_id) - if not product_id: - fpos = self.env['account.fiscal.position'].browse(fposition_id) - unique_tax_ids = fpos.map_tax(account.tax_ids).ids - else: - product_change_result = self.product_id_change(product_id, False, type=inv_type, - partner_id=partner_id, fposition_id=fposition_id, company_id=account.company_id.id, date_invoice=date_invoice) - if 'invoice_line_tax_id' in product_change_result.get('value', {}): - unique_tax_ids = product_change_result['value']['invoice_line_tax_id'] - return {'value': {'invoice_line_tax_id': unique_tax_ids}} - - -class account_invoice_tax(models.Model): +class AccountInvoiceTax(models.Model): _name = "account.invoice.tax" _description = "Invoice Tax" _order = 'sequence' - @api.one - @api.depends('base', 'base_amount', 'amount', 'tax_amount') - def _compute_factors(self): - self.factor_base = self.base_amount / self.base if self.base else 1.0 - self.factor_tax = self.tax_amount / self.amount if self.amount else 1.0 - - invoice_id = fields.Many2one('account.invoice', string='Invoice Line', - ondelete='cascade', index=True) - name = fields.Char(string='Tax Description', - required=True) - account_id = fields.Many2one('account.account', string='Tax Account', - required=True, domain=[('type', 'not in', ['view', 'income', 'closed'])]) + invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True) + name = fields.Char(string='Tax Description', required=True) + tax_id = fields.Many2one('account.tax', string='Tax') + account_id = fields.Many2one('account.account', string='Tax Account', required=True, domain=[('deprecated', '=', False)]) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic account') - base = fields.Float(string='Base', digits=dp.get_precision('Account')) - amount = fields.Float(string='Amount', digits=dp.get_precision('Account')) - manual = fields.Boolean(string='Manual', default=True) - sequence = fields.Integer(string='Sequence', - help="Gives the sequence order when displaying a list of invoice tax.") - base_code_id = fields.Many2one('account.tax.code', string='Base Code', - help="The account basis of the tax declaration.") - base_amount = fields.Float(string='Base Code Amount', digits=dp.get_precision('Account'), - default=0.0) - tax_code_id = fields.Many2one('account.tax.code', string='Tax Code', - help="The tax basis of the tax declaration.") - tax_amount = fields.Float(string='Tax Code Amount', digits=dp.get_precision('Account'), - default=0.0) - - company_id = fields.Many2one('res.company', string='Company', - related='account_id.company_id', store=True, readonly=True) - factor_base = fields.Float(string='Multipication factor for Base code', - compute='_compute_factors') - factor_tax = fields.Float(string='Multipication factor Tax code', - compute='_compute_factors') - - @api.multi - def base_change(self, base, currency_id=False, company_id=False, date_invoice=False): - factor = self.factor_base if self else 1 - company = self.env['res.company'].browse(company_id) - if currency_id and company.currency_id: - currency = self.env['res.currency'].browse(currency_id) - currency = currency.with_context(date=date_invoice or fields.Date.context_today(self)) - base = currency.compute(base * factor, company.currency_id, round=False) - return {'value': {'base_amount': base}} - - @api.multi - def amount_change(self, amount, currency_id=False, company_id=False, date_invoice=False): - company = self.env['res.company'].browse(company_id) - if currency_id and company.currency_id: - currency = self.env['res.currency'].browse(currency_id) - currency = currency.with_context(date=date_invoice or fields.Date.context_today(self)) - amount = currency.compute(amount, company.currency_id, round=False) - return {'value': {'tax_amount': amount}} - - @api.v8 - def compute(self, invoice): - tax_grouped = {} - currency = invoice.currency_id.with_context(date=invoice.date_invoice or fields.Date.context_today(invoice)) - company_currency = invoice.company_id.currency_id - for line in invoice.invoice_line: - taxes = line.invoice_line_tax_id.compute_all( - (line.price_unit * (1 - (line.discount or 0.0) / 100.0)), - line.quantity, line.product_id, invoice.partner_id)['taxes'] - for tax in taxes: - val = { - 'invoice_id': invoice.id, - 'name': tax['name'], - 'amount': tax['amount'], - 'manual': False, - 'sequence': tax['sequence'], - 'base': currency.round(tax['price_unit'] * line['quantity']), - } - if invoice.type in ('out_invoice','in_invoice'): - val['base_code_id'] = tax['base_code_id'] - val['tax_code_id'] = tax['tax_code_id'] - val['base_amount'] = currency.compute(val['base'] * tax['base_sign'], company_currency, round=False) - val['tax_amount'] = currency.compute(val['amount'] * tax['tax_sign'], company_currency, round=False) - val['account_id'] = tax['account_collected_id'] or line.account_id.id - val['account_analytic_id'] = tax['account_analytic_collected_id'] - else: - val['base_code_id'] = tax['ref_base_code_id'] - val['tax_code_id'] = tax['ref_tax_code_id'] - val['base_amount'] = currency.compute(val['base'] * tax['ref_base_sign'], company_currency, round=False) - val['tax_amount'] = currency.compute(val['amount'] * tax['ref_tax_sign'], company_currency, round=False) - val['account_id'] = tax['account_paid_id'] or line.account_id.id - val['account_analytic_id'] = tax['account_analytic_paid_id'] - - # If the taxes generate moves on the same financial account as the invoice line - # and no default analytic account is defined at the tax level, propagate the - # analytic account from the invoice line to the tax line. This is necessary - # in situations were (part of) the taxes cannot be reclaimed, - # to ensure the tax move is allocated to the proper analytic account. - if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id: - val['account_analytic_id'] = line.account_analytic_id.id - - key = (val['tax_code_id'], val['base_code_id'], val['account_id']) - if not key in tax_grouped: - tax_grouped[key] = val - else: - tax_grouped[key]['base'] += val['base'] - tax_grouped[key]['amount'] += val['amount'] - tax_grouped[key]['base_amount'] += val['base_amount'] - tax_grouped[key]['tax_amount'] += val['tax_amount'] - - for t in tax_grouped.values(): - t['base'] = currency.round(t['base']) - t['amount'] = currency.round(t['amount']) - t['base_amount'] = currency.round(t['base_amount']) - t['tax_amount'] = currency.round(t['tax_amount']) - - return tax_grouped - - @api.v7 - def compute(self, cr, uid, invoice_id, context=None): - recs = self.browse(cr, uid, [], context) - invoice = recs.env['account.invoice'].browse(invoice_id) - return recs.compute(invoice) - - @api.model - def move_line_get(self, invoice_id): - res = [] - self._cr.execute( - 'SELECT * FROM account_invoice_tax WHERE invoice_id = %s', - (invoice_id,) - ) - for row in self._cr.dictfetchall(): - if not (row['amount'] or row['tax_code_id'] or row['tax_amount']): - continue - res.append({ - 'type': 'tax', - 'name': row['name'], - 'price_unit': row['amount'], - 'quantity': 1, - 'price': row['amount'] or 0.0, - 'account_id': row['account_id'], - 'tax_code_id': row['tax_code_id'], - 'tax_amount': row['tax_amount'], - 'account_analytic_id': row['account_analytic_id'], - }) - return res + amount = fields.Monetary() + manual = fields.Boolean(default=True) + sequence = fields.Integer(help="Gives the sequence order when displaying a list of invoice tax.") + company_id = fields.Many2one('res.company', string='Company', related='account_id.company_id', store=True, readonly=True) + currency_id = fields.Many2one('res.currency', related='invoice_id.currency_id', store=True, readonly=True) -class res_partner(models.Model): - # Inherits partner and adds invoice information in the partner form - _inherit = 'res.partner' +class AccountPaymentTerm(models.Model): + _name = "account.payment.term" + _description = "Payment Term" + _order = "name" - invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices', - readonly=True, copy=False) + name = fields.Char(string='Payment Term', translate=True, required=True) + active = fields.Boolean(default=True, help="If the active field is set to False, it will allow you to hide the payment term without removing it.") + note = fields.Text(string='Description', translate=True) + line_ids = fields.One2many('account.payment.term.line', 'payment_id', string='Terms', copy=True) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) - def _find_accounting_partner(self, partner): - ''' - Find the partner for which the accounting entries will be created - ''' - return partner.commercial_partner_id + @api.constrains('line_ids') + @api.one + def _check_lines(self): + if self.line_ids and self.line_ids[-1].value != 'balance': + raise UserError("A Payment Term should have its last line of type Balance") -class mail_compose_message(models.Model): + @api.one + def compute(self, value, date_ref=False): + date_ref = date_ref or datetime.now().strftime('%Y-%m-%d') + amount = value + result = [] + prec = self.company_id.currency_id.decimal_places + for line in self.line_ids: + if line.value == 'fixed': + amt = round(line.value_amount, prec) + elif line.value == 'percent': + amt = round(value * (line.value_amount / 100.0), prec) + elif line.value == 'balance': + amt = round(amount, prec) + if amt: + next_date = (datetime.strptime(date_ref, '%Y-%m-%d') + relativedelta(days=line.days)) + if line.days2 < 0: + next_first_date = next_date + relativedelta(day=1, months=1) # Getting 1st of next month + next_date = next_first_date + relativedelta(days=line.days2) + if line.days2 > 0: + next_date += relativedelta(day=line.days2, months=1) + result.append((next_date.strftime('%Y-%m-%d'), amt)) + amount -= amt + + amount = reduce(lambda x, y: x + y[1], result, 0.0) + dist = round(value - amount, prec) + if dist: + last_date = result and result[-1][0] or fields.Date.today() + result.append((last_date, dist)) + return result + + +class AccountPaymentTermLine(models.Model): + _name = "account.payment.term.line" + _description = "Payment Term Line" + _order = "days" + + value = fields.Selection([ + ('balance', 'Balance'), + ('percent', 'Percent'), + ('fixed', 'Fixed Amount') + ], string='Computation', required=True, default='balance', + help="Select here the kind of valuation related to this payment term line.") + value_amount = fields.Float(string='Amount To Pay', digits=dp.get_precision('Payment Term'), help="For percent enter a ratio between 0-100.") + days = fields.Integer(string='Number of Days', required=True, default=30, help="Number of days to add before computing the day of the month.") + days2 = fields.Integer(string='Day of the Month', required=True, default='0', + help="Day of the month \n\n Set : \n1)-1 for the last day of the current month. \n2) 0 for net days\n3) A positive number for the specific day of the next month.\n\nExample : if Date=15/01, Number of Days=22, Day of Month=-1, then the due date is 28/02.") + payment_id = fields.Many2one('account.payment.term', string='Payment Term', required=True, index=True, ondelete='cascade') + + @api.one + @api.constrains('value', 'value_amount') + def _check_percent(self): + if self.value == 'percent' and (self.value_amount < 0.0 or self.value_amount > 100.0): + raise UserError(_('Percentages for Payment Term Line must be between 0 and 100.')) + + +class MailComposeMessage(models.Model): _inherit = 'mail.compose.message' @api.multi @@ -1657,4 +1271,4 @@ class mail_compose_message(models.Model): invoice = invoice.with_context(mail_post_autofollow=True) invoice.write({'sent': True}) invoice.message_post(body=_("Invoice sent")) - return super(mail_compose_message, self).send_mail() + return super(MailComposeMessage, self).send_mail() diff --git a/addons/account/account_journal_dashboard.py b/addons/account/account_journal_dashboard.py new file mode 100644 index 00000000000..706c5c9c6a3 --- /dev/null +++ b/addons/account/account_journal_dashboard.py @@ -0,0 +1,313 @@ +import json +from datetime import datetime, timedelta + +from babel.dates import format_datetime, format_date + +from openerp import models, api, _, fields +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT +from openerp.tools.misc import formatLang + +class account_journal(models.Model): + _inherit = "account.journal" + + @api.one + def _kanban_dashboard(self): + self.kanban_dashboard = json.dumps(self.get_journal_dashboard_datas()) + + @api.one + def _kanban_dashboard_graph(self): + if (self.type in ['sale', 'purchase']): + self.kanban_dashboard_graph = json.dumps(self.get_bar_graph_datas()) + elif (self.type in ['cash', 'bank']): + self.kanban_dashboard_graph = json.dumps(self.get_line_graph_datas()) + + kanban_dashboard = fields.Text(compute='_kanban_dashboard') + kanban_dashboard_graph = fields.Text(compute='_kanban_dashboard_graph') + show_on_dashboard = fields.Boolean(string='Show journal on dashboard', help="Whether this journal should be displayed on the dashboard or not", default=True) + + @api.multi + def toggle_favorite(self): + self.write({'show_on_dashboard': False if self.show_on_dashboard else True}) + return False + + @api.multi + def get_line_graph_datas(self): + data = [] + today = datetime.today() + last_month = today + timedelta(days=-30) + bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids),('date', '>', last_month.strftime(DEFAULT_SERVER_DATE_FORMAT)),('date', '<=', today.strftime(DEFAULT_SERVER_DATE_FORMAT))], order="date asc") + last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids),('date', '<=', last_month.strftime(DEFAULT_SERVER_DATE_FORMAT))], order="date desc", limit=1) + start_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0 + locale = self._context.get('lang', 'en_US') + show_date = last_month + #get date in locale format + name = format_date(show_date, 'd LLLL Y', locale=locale) + short_name = format_date(show_date, 'd MMM', locale=locale) + data.append({'x':short_name,'y':start_balance, 'name':name}) + + for stmt in bank_stmt: + #fill the gap between last data and the new one + number_day_to_add = (datetime.strptime(stmt.date, DEFAULT_SERVER_DATE_FORMAT) - show_date).days + last_balance = data[len(data) - 1]['y'] + for day in range(0,number_day_to_add + 1): + show_date = show_date + timedelta(days=1) + #get date in locale format + name = format_date(show_date, 'd LLLL Y', locale=locale) + short_name = format_date(show_date, 'd MMM', locale=locale) + data.append({'x': short_name, 'y':last_balance, 'name': name}) + #add new stmt value + data[len(data) - 1]['y'] = stmt.balance_end + + #continue the graph if the last statement isn't today + if show_date != today: + number_day_to_add = (today - show_date).days + last_balance = data[len(data) - 1]['y'] + for day in range(0,number_day_to_add): + show_date = show_date + timedelta(days=1) + #get date in locale format + name = format_date(show_date, 'd LLLL Y', locale=locale) + short_name = format_date(show_date, 'd MMM', locale=locale) + data.append({'x': short_name, 'y':last_balance, 'name': name}) + + return [{'values': data, 'color': '#ff7f0e', 'area': True}] + + @api.multi + def get_bar_graph_datas(self): + data = [] + title = _('Invoices owed to you') + if self.type == 'purchase': + title = _('Bills you need to pay') + today = datetime.today() + data.append({'label': _('Past'), 'value':0.0, 'color': '#ff0000'}) + day_of_week = int(format_datetime(today, 'e', locale=self._context.get('lang', 'en_US'))) + first_day_of_week = today + timedelta(days=-day_of_week+1) + for i in range(-1,4): + if i==0: + label = _('This Week') + elif i==3: + label = _('Future') + else: + start_week = first_day_of_week + timedelta(days=i*7) + end_week = start_week + timedelta(days=6) + if start_week.month == end_week.month: + label = str(start_week.day) + '-' +str(end_week.day)+ ' ' + format_date(end_week, 'MMM', locale=self._context.get('lang', 'en_US')) + else: + label = format_date(start_week, 'd MMM', locale=self._context.get('lang', 'en_US'))+'-'+format_date(end_week, 'd MMM', locale=self._context.get('lang', 'en_US')) + data.append({'label':label,'value':0.0, 'color': '#ff0000' if i<0 else '#ff7f0e'}) + for invoice in self.env['account.invoice'].search([('journal_id','=',self.id),('state', '=', 'open')]): + if invoice.date_due < (first_day_of_week + timedelta(days=-7)).strftime(DEFAULT_SERVER_DATE_FORMAT): + data[0]['value'] += invoice.residual_signed + elif invoice.date_due < first_day_of_week.strftime(DEFAULT_SERVER_DATE_FORMAT): + data[1]['value'] += invoice.residual_signed + elif invoice.date_due >= (first_day_of_week + timedelta(days=21)).strftime(DEFAULT_SERVER_DATE_FORMAT): + data[5]['value'] += invoice.residual_signed + elif invoice.date_due >= (first_day_of_week + timedelta(days=14)).strftime(DEFAULT_SERVER_DATE_FORMAT): + data[4]['value'] += invoice.residual_signed + elif invoice.date_due >= (first_day_of_week + timedelta(days=7)).strftime(DEFAULT_SERVER_DATE_FORMAT): + data[3]['value'] += invoice.residual_signed + else: + data[2]['value'] += invoice.residual_signed + #postprocess to set graph color + for bar in data: + if bar['value'] == 0.0: + bar['color'] = '#ffffff' + return [{'values': data, 'title': title}] + + @api.multi + def get_journal_dashboard_datas(self): + number_to_reconcile = last_balance = account_sum = 0 + ac_bnk_stmt = [] + number_draft = number_waiting = number_late = sum_draft = sum_waiting = sum_late = 0 + if self.type in ['bank', 'cash']: + last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)], order="date desc", limit=1) + last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0 + ac_bnk_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids),('state', '=', 'open')]) + for ac_bnk in ac_bnk_stmt: + for line in ac_bnk.line_ids: + if not line.journal_entry_ids: + number_to_reconcile += 1 + account = [self.default_debit_account_id.id, self.default_credit_account_id.id] + acm_lines = self.env['account.move.line'].search([('account_id', 'in', account)]) + account_sum = sum([line.balance for line in acm_lines]) or 0 + #TODO need to check if all invoices are in the same currency than the journal!!!! + elif self.type in ['sale', 'purchase']: + invoices = self.env['account.invoice'].search([('journal_id', 'in', self.ids), ('state', 'not in', ('paid', 'cancel'))]) + for invoice in invoices: + if invoice.state in ['draft', 'proforma', 'proforma2']: + number_draft += 1 + sum_draft += invoice.amount_total + else: + number_waiting += 1 + sum_waiting += invoice.residual + if invoice.date_due < fields.Date.today(): + number_late += 1 + sum_late += invoice.residual + + return { + 'number_to_reconcile': number_to_reconcile, + 'account_balance': formatLang(self.env, account_sum, currency_obj=self.currency_id or self.company_id.currency_id), + 'last_balance': formatLang(self.env, last_balance, currency_obj=self.currency_id or self.company_id.currency_id), + 'number_draft': number_draft, + 'number_waiting': number_waiting, + 'number_late': number_late, + 'sum_draft': formatLang(self.env, sum_draft, currency_obj=self.currency_id or self.company_id.currency_id), + 'sum_waiting': formatLang(self.env, sum_waiting, currency_obj=self.currency_id or self.company_id.currency_id), + 'sum_late': formatLang(self.env, sum_late, currency_obj=self.currency_id or self.company_id.currency_id), + 'currency_id': self.currency_id and self.currency_id.id or self.company_id.currency_id.id, + 'show_import': True if self.type in ['bank', 'cash'] and len(ac_bnk_stmt) == 0 and last_balance == 0 else False, + } + + @api.multi + def action_create_new(self): + ctx = self._context.copy() + model = 'account.invoice' + if self.type == 'sale': + ctx.update({'journal_type': self.type, 'default_type': 'out_invoice', 'type': 'out_invoice', 'default_journal_id': self.id}) + if ctx.get('refund'): + ctx.update({'default_type':'out_refund', 'type':'out_refund'}) + view_id = self.env.ref('account.invoice_form').id + elif self.type == 'purchase': + ctx.update({'journal_type': self.type, 'default_type': 'in_invoice', 'type': 'in_invoice', 'default_journal_id': self.id}) + if ctx.get('refund'): + ctx.update({'default_type': 'in_refund', 'type': 'in_refund'}) + view_id = self.env.ref('account.invoice_supplier_form').id + else: + ctx.update({'default_journal_id': self.id}) + view_id = self.env.ref('account.view_move_form').id + model = 'account.move' + return { + 'name': _('Create invoice/bill'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': model, + 'view_id': view_id, + 'context': ctx, + } + + @api.multi + def create_cash_bank(self): + ctx = self._context.copy() + ctx.update({'journal_id': self.id, 'default_journal_id': self.id, 'default_journal_type': 'cash'}) + return { + 'name': _('Create cash statement'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'account.bank.statement', + 'context': ctx, + } + + @api.multi + def action_open_reconcile(self): + if self.type in ['bank', 'cash']: + # Open reconciliation view for bank statements belonging to this journal + bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)]) + return { + 'type': 'ir.actions.client', + 'tag': 'bank_statement_reconciliation_view', + 'context': {'statement_ids': bank_stmt.ids}, + } + else: + # Open reconciliation view for customers/suppliers + action_context = {'show_mode_selector': False} + if self.type == 'sale': + action_context.update({'mode': 'customers'}) + elif self.type == 'purchase': + action_context.update({'mode': 'suppliers'}) + return { + 'type': 'ir.actions.client', + 'tag': 'manual_reconciliation_view', + 'context': action_context, + } + + @api.multi + def open_action(self): + """return action based on type for related journals""" + action_name = self._context.get('action_name', False) + if not action_name: + if self.type == 'bank': + action_name = 'action_bank_statement_tree' + elif self.type == 'cash': + action_name = 'action_view_bank_statement_tree' + elif self.type == 'sale': + action_name = 'action_invoice_tree1' + elif self.type == 'purchase': + action_name = 'action_invoice_tree2' + else: + action_name = 'action_move_journal_line' + + _journal_invoice_type_map = { + 'sale': 'out_invoice', + 'purchase': 'in_invoice', + 'bank': 'bank', + 'cash': 'cash', + 'general': 'general', + } + invoice_type = _journal_invoice_type_map[self.type] + + ctx = self._context.copy() + ctx.update({ + 'journal_type': self.type, + 'default_journal_id': self.id, + 'search_default_journal_id': self.id, + 'default_type': invoice_type, + 'type': invoice_type + }) + ir_model_obj = self.pool['ir.model.data'] + model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name) + action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context) + action['context'] = ctx + return action + + @api.multi + def open_spend_money(self): + return self.open_payments_action('outbound') + + @api.multi + def open_collect_money(self): + return self.open_payments_action('inbound') + + @api.multi + def open_transfer_money(self): + return self.open_payments_action('transfer') + + @api.multi + def open_payments_action(self, payment_type): + ctx = self._context.copy() + ctx.update({ + 'default_payment_type': payment_type, + 'default_journal_id': self.id + }) + action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_account_payments') + if action_rec: + action = action_rec.read([])[0] + action['context'] = ctx + return action + + @api.multi + def open_action_with_context(self): + action_name = self.env.context.get('action_name', False) + if not action_name: + return False + ctx = dict(self.env.context, default_journal_id=self.id) + if ctx.get('search_default_journal', False): + ctx.update(search_default_journal_id=self.id) + ir_model_obj = self.pool['ir.model.data'] + model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name) + action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context) + action['context'] = ctx + if ctx.get('use_domain', False): + action['domain'] = ['|', ('journal_id', '=', self.id), ('journal_id', '=', False)] + action['name'] += ' for journal '+self.name + return action + + @api.multi + def import_statement(self): + """return action to import bank/cash statements. This button should be called only on journals with type =='bank'""" + model = 'account.bank.statement' + action_name = 'action_account_bank_statement_import' + ir_model_obj = self.pool['ir.model.data'] + model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account_bank_statement_import', action_name) + action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context) + return action diff --git a/addons/account/account_menuitem.xml b/addons/account/account_menuitem.xml deleted file mode 100644 index b5496cf6f5e..00000000000 --- a/addons/account/account_menuitem.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/account/account_move.py b/addons/account/account_move.py new file mode 100644 index 00000000000..299cd4fae28 --- /dev/null +++ b/addons/account/account_move.py @@ -0,0 +1,1188 @@ +# -*- coding: utf-8 -*- + +import time +from openerp import api, fields, models, _ +from openerp.osv import expression +from openerp.exceptions import RedirectWarning, UserError +from openerp.tools.misc import formatLang +from openerp.tools import float_is_zero +from openerp.tools.safe_eval import safe_eval + + +#---------------------------------------------------------- +# Entries +#---------------------------------------------------------- + + +class AccountMove(models.Model): + _name = "account.move" + _description = "Account Entry" + _order = 'date desc, id desc' + + @api.multi + @api.depends('name', 'state') + def name_get(self): + result = [] + for move in self: + if move.state == 'draft': + name = '* ' + str(move.id) + else: + name = move.name + result.append((move.id, name)) + return result + + @api.multi + @api.depends('line_ids.debit', 'line_ids.credit') + def _amount_compute(self): + for move in self: + total = 0.0 + for line in move.line_ids: + total += line.debit + move.amount = total + + @api.depends('line_ids.debit', 'line_ids.credit', 'line_ids.matched_debit_ids.amount', 'line_ids.matched_credit_ids.amount', 'line_ids.account_id.user_type.type') + def _compute_matched_percentage(self): + """Compute the percentage to apply for cash basis method. This value is relevant only for moves that + involve journal items on receivable or payable accounts. + """ + for move in self: + total_amount = 0.0 + total_reconciled = 0.0 + for line in move.line_ids: + if line.account_id.user_type.type in ('receivable', 'payable'): + amount = abs(line.debit - line.credit) + total_amount += amount + for partial_line in (line.matched_debit_ids + line.matched_credit_ids): + total_reconciled += partial_line.amount + if total_amount == 0.0: + move.matched_percentage = 100.0 + else: + move.matched_percentage = total_reconciled / total_amount + + @api.one + @api.depends('company_id') + def _compute_currency(self): + self.currency_id = self.company_id.currency_id or self.env.user.company_id.currency_id + + name = fields.Char(string='Number', required=True, copy=False, default='/') + ref = fields.Char(string='Partner Reference', copy=False) + date = fields.Date(required=True, states={'posted': [('readonly', True)]}, index=True, default=fields.Date.context_today) + journal_id = fields.Many2one('account.journal', string='Journal', required=True, states={'posted': [('readonly', True)]}) + currency_id = fields.Many2one('res.currency', compute='_compute_currency', store=True) + rate_diff_partial_rec_id = fields.Many2one('account.partial.reconcile', string='Exchange Rate Entry of', help="Technical field used to keep track of the origin of journal entries created in case of fluctuation of the currency exchange rate. This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.") + state = fields.Selection([('draft', 'Unposted'), ('posted', 'Posted')], string='Status', + required=True, readonly=True, copy=False, default='draft', + help='All manually created new journal entries are usually in the status \'Unposted\', ' + 'but you can set the option to skip that status on the related journal. ' + 'In that case, they will behave as journal entries automatically created by the ' + 'system on document validation (invoices, bank statements...) and will be created ' + 'in \'Posted\' status.') + line_ids = fields.One2many('account.move.line', 'move_id', string='Journal Items', + states={'posted': [('readonly', True)]}, copy=True) + partner_id = fields.Many2one('res.partner', related='line_ids.partner_id', string="Partner", store=True, states={'posted': [('readonly', True)]}) + amount = fields.Monetary(compute='_amount_compute', store=True) + narration = fields.Text(string='Internal Note') + company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, + default=lambda self: self.env.user.company_id) + matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") + statement_line_id = fields.Many2one('account.bank.statement.line', string='Bank statement line reconciled with this entry', copy=False, readonly=True) + to_check = fields.Boolean('To Review', help='Check this box if you are unsure of that journal entry and if you want to note it as \'to be reviewed\' by an accounting expert.') + + @api.model + def create(self, vals): + move = super(AccountMove, self.with_context(check_move_validity=False)).create(vals) + move.assert_balanced() + return move + + @api.multi + def write(self, vals): + if 'line_ids' in vals: + res = super(AccountMove, self.with_context(check_move_validity=False)).write(vals) + self.assert_balanced() + else: + res = super(AccountMove, self).write(vals) + return res + + @api.multi + def post(self): + invoice = self._context.get('invoice', False) + self._post_validate() + + for move in self: + move.line_ids.create_analytic_lines() + if move.name == '/': + new_name = False + journal = move.journal_id + + if invoice and invoice.move_name: + new_name = invoice.move_name + else: + if journal.sequence_id: + # If invoice is actually refund and journal has a refund_sequence then use that one or use the regular one + sequence = journal.sequence_id + if invoice and invoice.type in ['out_refund', 'in_refund'] and journal.refund_sequence: + sequence = journal.refund_sequence_id + new_name = sequence.with_context(ir_sequence_date=move.date).next_by_id() + else: + raise UserError(_('Please define a sequence on the journal.')) + + if new_name: + move.name = new_name + return self.write({'state': 'posted'}) + + @api.multi + def button_cancel(self): + for move in self: + if not move.journal_id.update_posted: + raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) + if self.ids: + self._cr.execute('UPDATE account_move '\ + 'SET state=%s '\ + 'WHERE id IN %s', ('draft', tuple(self.ids),)) + self.invalidate_cache() + return True + + @api.multi + def unlink(self): + for move in self: + #check the lock date + check if some entries are reconciled + move.line_ids._update_check() + move.line_ids.unlink() + return super(AccountMove, self).unlink() + + @api.multi + def _post_validate(self): + for move in self: + if move.line_ids: + if not all([x.company_id.id == move.company_id.id for x in move.line_ids]): + raise UserError(_("Cannot create moves for different companies.")) + self.assert_balanced() + return self._check_lock_date() + + @api.multi + def _check_lock_date(self): + for move in self: + lock_date = max(move.company_id.period_lock_date, move.company_id.fiscalyear_lock_date) + if self.user_has_groups('account.group_account_manager'): + lock_date = move.company_id.fiscalyear_lock_date + if move.date <= lock_date: + raise UserError(_("You cannot add/modify entries prior to and inclusive of the lock date %s. Check the company settings or ask someone with the 'Adviser' role" % (lock_date))) + return True + + @api.multi + def assert_balanced(self): + if not self.ids: + return True + self._cr.execute("""\ + SELECT move_id + FROM account_move_line + WHERE move_id in %s + GROUP BY move_id + HAVING abs(sum(debit) - sum(credit)) > 0.00001 + """, (tuple(self.ids),)) + if len(self._cr.fetchall()) != 0: + raise UserError(_("Cannot create unbalanced journal entry.")) + return True + + @api.multi + def reverse_moves(self, date=None, journal_id=None): + date = date or fields.Date.today() + for ac_move in self: + reversed_move = ac_move.copy(default={'date': date, + 'journal_id': journal_id.id if journal_id else ac_move.journal_id.id, + 'ref': _('reversal of: ') + ac_move.name}) + for acm_line in reversed_move.line_ids: + acm_line.with_context(check_move_validity=False).write({ + 'debit': acm_line.credit, + 'credit': acm_line.debit, + 'amount_currency': -acm_line.amount_currency + }) + reversed_move._post_validate() + reversed_move.post() + return True + + +class AccountMoveLine(models.Model): + _name = "account.move.line" + _description = "Journal Item" + _order = "date desc, id desc" + + @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id') + def _amount_residual(self): + """ Computes the residual amount of a move line from a reconciliable account in the company currency and the line's currency. + This amount will be 0 for fully reconciled lines or lines from a non-reconciliable account, the original line amount + for unreconciled lines, and something in-between for partially reconciled lines. + """ + for line in self: + #amounts in the partial reconcile table aren't signed, so we need to use abs() + amount = abs(line.debit - line.credit) + amount_residual_currency = abs(line.amount_currency) or 0.0 + sign = 1 if (line.debit - line.credit) > 0 else -1 + + for partial_line in (line.matched_debit_ids + line.matched_credit_ids): + amount -= partial_line.amount + #getting the date of the matched item to compute the amount_residual in currency + date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date + if line.currency_id: + if partial_line.currency_id and partial_line.currency_id == line.currency_id: + amount_residual_currency -= partial_line.amount_currency + else: + amount_residual_currency -= line.company_id.currency_id.with_context(date=date).compute(partial_line.amount, line.currency_id) + + #computing the `reconciled` field. As we book exchange rate difference on each partial matching, + #we can only check the amount in company currency + reconciled = False + digits_rounding_precision = line.company_id.currency_id.rounding + if float_is_zero(amount, digits_rounding_precision): + reconciled = True + line.reconciled = reconciled + + line.amount_residual = line.company_id.currency_id.round(amount * sign) + line.amount_residual_currency = line.currency_id and line.currency_id.round(amount_residual_currency * sign) or 0.0 + + @api.depends('debit', 'credit') + def _store_balance(self): + for line in self: + line.balance = line.debit - line.credit + + @api.model + def _get_currency(self): + currency = False + context = self._context or {} + if context.get('default_journal_id', False): + currency = self.env['account.journal'].browse(context['default_journal_id']).currency + return currency + + @api.model + def _get_journal(self): + """ Return journal based on the journal type """ + context = dict(self._context or {}) + journal_id = context.get('journal_id', False) + if journal_id: + return journal_id + + journal_type = context.get('journal_type', False) + if journal_type: + recs = self.env['account.journal'].search([('type', '=', journal_type)]) + if not recs: + action = self.env.ref('account.action_account_journal_form') + msg = _("""Cannot find any account journal of "%s" type for this company, You should create one.\n Please go to Journal Configuration""") % journal_type.replace('_', ' ').title() + raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) + journal_id = recs[0].id + return journal_id + + @api.depends('debit', 'credit', 'move_id.matched_percentage', 'move_id.journal_id') + def _compute_cash_basis(self): + for move_line in self: + if move_line.journal_id.type in ('sale', 'purchase'): + move_line.debit_cash_basis = move_line.debit * move_line.move_id.matched_percentage + move_line.credit_cash_basis = move_line.credit * move_line.move_id.matched_percentage + else: + move_line.debit_cash_basis = move_line.debit + move_line.credit_cash_basis = move_line.credit + move_line.balance_cash_basis = move_line.debit_cash_basis - move_line.credit_cash_basis + + @api.one + @api.depends('move_id.line_ids') + def _get_counterpart(self): + counterpart = set() + for line in self.move_id.line_ids: + if (line.account_id.code != self.account_id.code): + counterpart.add(line.account_id.code) + if len(counterpart) > 2: + counterpart = counterpart[0:2] + ["..."] + self.counterpart = ",".join(counterpart) + + name = fields.Char(required=True) + quantity = fields.Float(digits=(16, 2), + help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports.") + product_uom_id = fields.Many2one('product.uom', string='Unit of Measure') + product_id = fields.Many2one('product.product', string='Product') + debit = fields.Monetary(default=0.0, currency_field='company_currency_id') + credit = fields.Monetary(default=0.0, currency_field='company_currency_id') + balance = fields.Monetary(compute='_store_balance', store=True, currency_field='company_currency_id', default=0.0, help="Technical field holding the debit - credit in order to open meaningful graph views from reports") + debit_cash_basis = fields.Monetary(currency_field='company_currency_id', default=0.0, compute='_compute_cash_basis', store=True) + credit_cash_basis = fields.Monetary(currency_field='company_currency_id', default=0.0, compute='_compute_cash_basis', store=True) + balance_cash_basis = fields.Monetary(compute='_compute_cash_basis', store=True, currency_field='company_currency_id', default=0.0, help="Technical field holding the debit_cash_basis - credit_cash_basis in order to open meaningful graph views from reports") + amount_currency = fields.Monetary(default=0.0, help="The amount expressed in an optional other currency if it is a multi-currency entry.") + company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, + help='Utility field to express amount currency', store=True) + currency_id = fields.Many2one('res.currency', string='Currency', default=_get_currency, + help="The optional other currency if it is a multi-currency entry.") + amount_residual = fields.Monetary(compute='_amount_residual', string='Residual Amount', store=True, currency_field='company_currency_id', + help="The residual amount on a journal item expressed in the company currency.") + amount_residual_currency = fields.Monetary(compute='_amount_residual', string='Residual Amount in Currency', store=True, + help="The residual amount on a journal item expressed in its currency (possibly not the company currency).") + account_id = fields.Many2one('account.account', string='Account', required=True, index=True, + ondelete="cascade", domain=[('deprecated', '=', False)], default=lambda self: self._context.get('account_id', False)) + move_id = fields.Many2one('account.move', string='Journal Entry', ondelete="cascade", + help="The move of this entry line.", index=True, required=True, auto_join=True) + narration = fields.Text(related='move_id.narration', string='Internal Note') + ref = fields.Char(related='move_id.ref', string='Partner Reference', store=True, copy=False) + payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry") + statement_id = fields.Many2one('account.bank.statement', string='Statement', + help="The bank statement used for bank reconciliation", index=True, copy=False) + reconciled = fields.Boolean(compute='_amount_residual', store=True) + matched_debit_ids = fields.One2many('account.partial.reconcile', 'credit_move_id', String='Matched Debits', + help='Debit journal items that are matched with this journal item.') + matched_credit_ids = fields.One2many('account.partial.reconcile', 'debit_move_id', String='Matched Credits', + help='Credit journal items that are matched with this journal item.') + journal_id = fields.Many2one('account.journal', related='move_id.journal_id', string='Journal', + default=_get_journal, required=True, index=True, store=True, copy=False) + blocked = fields.Boolean(string='No Follow-up', default=False, + help="You can check this box to mark this journal item as a litigation with the associated partner") + date_maturity = fields.Date(string='Due date', index=True, + help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.") + date = fields.Date(related='move_id.date', string='Effective date', required=True, index=True, default=fields.Date.context_today, store=True, copy=False) + analytic_lines = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines') + tax_ids = fields.Many2many('account.tax', string='Taxes', copy=False, readonly=True) + tax_line_id = fields.Many2one('account.tax', string='Originator tax', copy=False, readonly=True) + analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account') + company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True) + counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.") + + # TODO: put the invoice link and partner_id on the account_move + invoice = fields.Many2one('account.invoice') + partner_id = fields.Many2one('res.partner', string='Partner', index=True, ondelete='restrict') + user_type = fields.Many2one('account.account.type', related='account_id.user_type', index=True, store=True) + + _sql_constraints = [ + ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'), + ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'), + ] + + @api.multi + @api.constrains('currency_id', 'account_id') + def _check_currency(self): + for line in self: + if line.account_id.currency_id: + if not line.currency_id or line.currency_id.id != line.account_id.currency_id.id: + raise UserError(_('The selected account of your Journal Entry forces to provide a secondary currency. You should remove the secondary currency on the account or select a multi-currency view on the journal.')) + + @api.multi + @api.constrains('currency_id', 'amount_currency') + def _check_currency_and_amount(self): + for line in self: + if (line.amount_currency and not line.currency_id): + raise UserError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field.")) + + @api.multi + @api.constrains('amount_currency') + def _check_currency_amount(self): + for line in self: + if line.amount_currency: + if (line.amount_currency > 0.0 and line.credit > 0.0) or (line.amount_currency < 0.0 and line.debit > 0.0): + raise UserError(_('The amount expressed in the secondary currency must be positive when account is debited and negative when account is credited.')) + + #################################################### + # Reconciliation interface methods + #################################################### + + @api.model + def get_data_for_manual_reconciliation_widget(self, partner_ids, account_ids): + """ Returns the data required for the invoices & payments matching of partners/accounts. + If an argument is None, fetch all related reconciliations. Use [] to fetch nothing. + """ + return { + 'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'), + 'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'), + 'accounts': self.get_data_for_manual_reconciliation('account', account_ids), + } + + @api.model + def get_data_for_manual_reconciliation(self, res_type, res_ids=None, account_type=None): + """ Returns the data required for the invoices & payments matching of partners/accounts (list of dicts). + If no res_ids is passed, returns data for all partners/accounts that can be reconciled. + + :param res_type: either 'partner' or 'account' + :param res_ids: ids of the partners/accounts to reconcile, use None to fetch data indiscriminately + of the id, use [] to prevent from fetching any data at all. + :param account_type: if a partner is both customer and supplier, you can use 'payable' to reconcile + the supplier-related journal entries and 'receivable' for the customer-related entries. + """ + if res_ids != None and len(res_ids) == 0: + # Note : this short-circuiting is better for performances, but also required + # since postgresql doesn't implement empty list (so 'AND id in ()' is useless) + return [] + + is_partner = res_type == 'partner' + res_alias = is_partner and 'p' or 'a' + self.env.cr.execute( + """ SELECT %s account_id, account_name, account_code, max_date, to_char(last_time_entries_checked, 'YYYY-MM-DD') AS last_time_entries_checked FROM ( + SELECT %s + %s.last_time_entries_checked AS last_time_entries_checked, + a.id AS account_id, + a.name AS account_name, + a.code AS account_code, + MAX(l.write_date) AS max_date + FROM + account_move_line l + RIGHT JOIN account_account a ON (a.id = l.account_id) + RIGHT JOIN account_account_type at ON (at.id = a.user_type) + %s + WHERE + a.reconcile IS TRUE + %s + %s + %s + AND EXISTS ( + SELECT NULL + FROM account_move_line l + WHERE l.account_id = a.id + %s + AND l.amount_residual > 0 + ) + AND EXISTS ( + SELECT NULL + FROM account_move_line l + WHERE l.account_id = a.id + %s + AND l.amount_residual < 0 + ) + GROUP BY %s a.id, a.name, a.code, %s.last_time_entries_checked + ORDER BY %s.last_time_entries_checked + ) as s + WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) + """ % ( + is_partner and 'partner_id, partner_name,' or ' ', + is_partner and 'p.id AS partner_id, p.name AS partner_name,' or ' ', + res_alias, + is_partner and 'RIGHT JOIN res_partner p ON (l.partner_id = p.id)' or ' ', + is_partner and ' ' or "AND at.type <> 'payable' AND at.type <> 'receivable'", + account_type and "AND at.type = '" + account_type + "'" or '', + res_ids and 'AND ' + res_alias + '.id in (' + ','.join(map(str, res_ids)) + ')' or '', + is_partner and 'AND l.partner_id = p.id' or ' ', + is_partner and 'AND l.partner_id = p.id' or ' ', + is_partner and 'l.partner_id, p.id,' or ' ', + res_alias, + res_alias, + )) + + # Apply ir_rules by filtering out + rows = self.env.cr.dictfetchall() + ids = [x['account_id'] for x in rows] + allowed_ids = set(self.env['account.account'].browse(ids).ids) + rows = [row for row in rows if row['account_id'] in allowed_ids] + if is_partner: + ids = [x['partner_id'] for x in rows] + allowed_ids = set(self.env['res.partner'].browse(ids).ids) + rows = [row for row in rows if row['partner_id'] in allowed_ids] + + # Fetch other data + for row in rows: + account = self.env['account.account'].browse(row['account_id']) + row['currency_id'] = account.currency_id.id or account.company_id.currency_id.id + partner_id = is_partner and row['partner_id'] or None + row['reconciliation_proposition'] = self.get_reconciliation_proposition(account.id, partner_id) + return rows + + @api.model + def get_reconciliation_proposition(self, account_id, partner_id=False): + """ Returns two lines whose amount are opposite """ + + # Get pairs + partner_id_condition = partner_id and 'AND a.partner_id = %d AND b.partner_id = %d' % (partner_id, partner_id) or '' + self.env.cr.execute( + """ SELECT a.id, b.id + FROM account_move_line a, account_move_line b + WHERE a.amount_residual = -b.amount_residual + AND NOT a.reconciled AND NOT b.reconciled + AND a.account_id = %d AND b.account_id = %d + %s + ORDER BY a.date asc + LIMIT 10 + """ % (account_id, account_id, partner_id_condition)) + pairs = self.env.cr.fetchall() + + # Apply ir_rules by filtering out + all_pair_ids = [element for tupl in pairs for element in tupl] + allowed_ids = set(self.env['account.move.line'].browse(all_pair_ids).ids) + pairs = [pair for pair in pairs if pair[0] in allowed_ids and pair[1] in allowed_ids] + + # Return lines formatted + if len(pairs) > 0: + target_currency = self.currency_id or self.company_id.currency_id + lines = self.browse(list(pairs[0])) + return lines.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency) + return [] + + @api.model + def domain_move_lines_for_reconciliation(self, excluded_ids=None, str=False): + """ Returns the domain which is common to both manual and bank statement reconciliation. + + :param excluded_ids: list of ids of move lines that should not be fetched + :param str: search string + """ + context = (self._context or {}) + if excluded_ids is None: + excluded_ids = [] + domain = [] + + if excluded_ids: + domain = expression.AND([domain, [('id', 'not in', excluded_ids)]]) + if str: + str_domain = [ + '|', ('move_id.name', 'ilike', str), + '|', ('move_id.ref', 'ilike', str), + '|', ('date_maturity', 'like', str), + '&', ('name', '!=', '/'), ('name', 'ilike', str) + ] + try: + amount = float(str) + amount_domain = ['|', ('amount_residual', '=', amount), '|', ('amount_residual_currency', '=', amount), '|', ('amount_residual', '=', -amount), ('amount_residual_currency', '=', -amount)] + str_domain = expression.OR([str_domain, amount_domain]) + except: + pass + + # When building a domain for the bank statement reconciliation, if there's no partner + # and a search string, search also a match in the partner names + if 'bank_statement_line' in context and not context['bank_statement_line'].partner_id.id: + str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]]) + + domain = expression.AND([domain, str_domain]) + return domain + + def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False): + """ Create domain criteria that are relevant to manual reconciliation. """ + domain = ['&', ('reconciled', '=', False), ('account_id', '=', account_id)] + if partner_id: + domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) + generic_domain = self.domain_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str) + + return expression.AND([generic_domain, domain]) + + @api.model + def get_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False, offset=0, limit=None, target_currency_id=False): + """ Returns unreconciled move lines for an account or a partner+account, formatted for the manual reconciliation widget """ + domain = self._domain_move_lines_for_manual_reconciliation(account_id, partner_id, excluded_ids, str) + lines = self.search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") + if target_currency_id: + target_currency = self.env['res.currency'].browse(target_currency_id) + else: + account = self.env['account.account'].browse(account_id) + target_currency = account.currency_id or account.company_id.currency_id + return lines.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency) + + @api.v7 + def prepare_move_lines_for_reconciliation_widget(self, cr, uid, line_ids, target_currency_id=False, context=None): + target_currency = target_currency_id and self.pool.get('res.currency').browse(cr, uid, target_currency_id, context=context) or False + return self.browse(cr, uid, line_ids, context).prepare_move_lines_for_reconciliation_widget(target_currency=target_currency) + + @api.v8 + def prepare_move_lines_for_reconciliation_widget(self, target_currency=False, target_date=False): + """ Returns move lines formatted for the manual/bank reconciliation widget + + :param target_currency: curreny you want the move line debit/credit converted into + :param target_date: date to use for the monetary conversion + """ + context = dict(self._context or {}) + ret = [] + + for line in self: + company_currency = line.account_id.company_id.currency_id + ret_line = { + 'id': line.id, + 'name': line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name, + 'ref': line.move_id.ref or '', + # For reconciliation between statement transactions and already registered payments (eg. checks) + # NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled + 'already_paid': line.account_id.internal_type == 'liquidity', + 'account_code': line.account_id.code, + 'account_name': line.account_id.name, + 'account_type': line.account_id.internal_type, + 'date_maturity': line.date_maturity, + 'date': line.date, + 'journal_name': line.journal_id.name, + 'partner_id': line.partner_id.id, + 'partner_name': line.partner_id.name, + 'currency_id': line.currency_id.id or False, + } + + debit = line.debit + credit = line.credit + amount = line.amount_residual + amount_currency = line.amount_residual_currency + + # For already reconciled lines, don't use amount_residual(_currency) + if line.account_id.internal_type == 'liquidity': + amount = abs(debit - credit) + amount_currency = abs(line.amount_currency) + + # Get right debit / credit: + target_currency = target_currency or company_currency + line_currency = line.currency_id or company_currency + amount_currency_str = "" + total_amount_currency_str = "" + if line_currency != company_currency: + total_amount = line.amount_currency + actual_debit = debit > 0 and amount_currency or 0.0 + actual_credit = credit > 0 and -amount_currency or 0.0 + else: + total_amount = abs(debit - credit) + actual_debit = debit > 0 and amount or 0.0 + actual_credit = credit > 0 and -amount or 0.0 + if line_currency != target_currency: + amount_currency_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=line_currency) + total_amount_currency_str = formatLang(self.env, total_amount, currency_obj=line_currency) + ctx = context.copy() + if target_date: + ctx.update({'date': target_date}) + total_amount = line_currency.with_context(ctx).compute(total_amount, target_currency) + actual_debit = line_currency.with_context(ctx).compute(actual_debit, target_currency) + actual_credit = line_currency.with_context(ctx).compute(actual_credit, target_currency) + amount_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=target_currency) + total_amount_str = formatLang(self.env, total_amount, currency_obj=target_currency) + + ret_line['debit'] = abs(actual_debit) + ret_line['credit'] = abs(actual_credit) + ret_line['amount_str'] = amount_str + ret_line['total_amount_str'] = total_amount_str + ret_line['amount_currency_str'] = amount_currency_str + ret_line['total_amount_currency_str'] = total_amount_currency_str + ret.append(ret_line) + return ret + + @api.v7 + def process_reconciliations(self, cr, uid, data, context=None): + """ Used to validate a batch of reconciliations in a single call + :param data: list of dicts containing: + - 'type': either 'partner' or 'account' + - 'id': id of the affected res.partner or account.account + - 'mv_line_ids': ids of exisiting account.move.line to reconcile + - 'new_mv_line_dicts': list of dicts containing values suitable for account_move_line.create() + """ + for datum in data: + if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2: + self.process_reconciliation(cr, uid, datum['mv_line_ids'], datum['new_mv_line_dicts'], context=context) + + if datum['type'] == 'partner': + self.env['res.partner'].browse(datum['id']).mark_as_reconciled() + if datum['type'] == 'account': + self.env['account.account'].browse(datum['id']).mark_as_reconciled() + + @api.v7 + def process_reconciliation(self, cr, uid, mv_line_ids, new_mv_line_dicts, context=None): + return self.browse(cr, uid, mv_line_ids, context).process_reconciliation(new_mv_line_dicts) + + @api.v8 + def process_reconciliation(self, new_mv_line_dicts): + """ Create new move lines from new_mv_line_dicts (if not empty) then call reconcile_partial on self and new move lines + + :param new_mv_line_dicts: list of dicts containing values suitable fot account_move_line.create() + """ + if len(self) < 1 or len(self) + len(new_mv_line_dicts) < 2: + raise UserError(_('Error!'), _('A reconciliation must involve at least 2 move lines.')) + + # Create writeoff move lines + if len(new_mv_line_dicts) > 0: + writeoff_lines = self.env['account.move.line'] + company_currency = self[0].account_id.company_id.currency_id + account_currency = self[0].account_id.currency_id or company_currency + for mv_line_dict in new_mv_line_dicts: + if account_currency != company_currency: + mv_line_dict['debit'] = account_currency.compute(mv_line_dict['debit'], company_currency) + mv_line_dict['credit'] = account_currency.compute(mv_line_dict['credit'], company_currency) + writeoff_lines += self._create_writeoff(mv_line_dict) + + (self + writeoff_lines).reconcile() + else: + self.reconcile() + + #################################################### + # Reconciliation methods + #################################################### + + def _get_pair_to_reconcile(self): + #field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set) + field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + #target the pair of move in self with smallest debit, credit + smallest_debit = smallest_credit = False + for aml in self: + if aml[field] > 0: + smallest_debit = (not smallest_debit or aml[field] < smallest_debit[field]) and aml or smallest_debit + elif aml[field] < 0: + smallest_credit = (not smallest_credit or aml[field] > smallest_credit[field]) and aml or smallest_credit + return smallest_debit, smallest_credit + + def auto_reconcile_lines(self): + """ This function iterates recursively on the recordset given as parameter as long as it + can find a debit and a credit to reconcile together. It returns the recordset of the + account move lines that were not reconciled during the process. + """ + if not self.ids: + return self + field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + sm_debit_move, sm_credit_move = self._get_pair_to_reconcile() + #there is no more pair to reconcile so return what move_line are left + if not sm_credit_move or not sm_debit_move: + return self + + #Reconcile the pair together + amount_reconcile = min(sm_debit_move[field], -sm_credit_move[field]) + #Remove from recordset the one(s) that will be totally reconciled + if amount_reconcile == sm_debit_move[field]: + self -= sm_debit_move + if amount_reconcile == -sm_credit_move[field]: + self -= sm_credit_move + + #Check for the currency and amount_currency we can set + currency = False + amount_reconcile_currency = 0 + if sm_debit_move.currency_id == sm_credit_move.currency_id and sm_debit_move.currency_id.id: + currency = sm_credit_move.currency_id.id + amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency) + + amount_reconcile = min(sm_debit_move.amount_residual, -sm_credit_move.amount_residual) + + self.env['account.partial.reconcile'].create({ + 'debit_move_id': sm_debit_move.id, + 'credit_move_id': sm_credit_move.id, + 'amount': amount_reconcile, + 'amount_currency': amount_reconcile_currency, + 'currency_id': currency, + }) + + #Iterate process again on self + return self.auto_reconcile_lines() + + @api.multi + def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False): + #Perform all checks on lines + company_ids = set() + all_accounts = [] + partners = set() + for line in self: + company_ids.add(line.company_id.id) + all_accounts.append(line.account_id) + if (line.account_id.internal_type in ('receivable', 'payable')): + partners.add(line.partner_id.id) + if line.reconciled: + raise UserError(_('You are trying to reconcile some entries that are already reconciled!')) + if len(company_ids) > 1: + raise UserError(_('To reconcile the entries company should be the same for all entries!')) + if len(set(all_accounts)) > 1: + raise UserError(_('Entries are not of the same account!')) + if not all_accounts[0].reconcile: + raise UserError(_('The account %s (%s) is not marked as reconciliable !') % (all_accounts[0].name, all_accounts[0].code)) + if len(partners) > 1: + raise UserError(_('The partner has to be the same on all lines for receivable and payable accounts!')) + + #reconcile everything that can be + remaining_moves = self.auto_reconcile_lines() + + #if writeoff_acc_id specified, then create write-off move with value the remaining amount from move in self + if writeoff_acc_id and writeoff_journal_id and remaining_moves: + writeoff_to_reconcile = remaining_moves._create_writeoff({'account_id': writeoff_acc_id.id, 'journal_id': writeoff_journal_id.id}) + #add writeoff line to reconcile algo and finish the reconciliation + remaining_moves = (remaining_moves + writeoff_to_reconcile).auto_reconcile_lines() + + def _create_writeoff(self, vals): + """ Create a writeoff move for the account.move.lines in self. If debit/credit is not specified in vals, + the writeoff amount will be computed as the sum of amount_residual of the given recordset. + + :param vals: dict containing values suitable fot account_move_line.create(). The data in vals will + be processed to create bot writeoff acount.move.line and their enclosing account.move. + """ + # Check and complete vals + if 'account_id' not in vals or 'journal_id' not in vals: + raise UserError(_("It is mandatory to specify an account and a journal to create a write-off.")) + if ('debit' in vals) ^ ('credit' in vals): + raise UserError(_("Either pass both debit and credit or none.")) + if 'date' not in vals: + vals['date'] = self._context.get('date_p') or time.strftime('%Y-%m-%d') + if 'name' not in vals: + vals['name'] = self._context.get('comment') or _('Write-Off') + #compute the writeoff amount if not given + if 'credit' not in vals and 'debit' not in vals: + amount = sum([r.amount_residual for r in self]) + vals['credit'] = amount > 0 and amount or 0.0 + vals['debit'] = amount < 0 and abs(amount) or 0.0 + company_currency = self[0].account_id.company_id.currency_id + account_currency = self[0].account_id.currency_id or company_currency + if 'amount_currency' not in vals and account_currency != company_currency: + vals['currency_id'] = account_currency + vals['amount_currency'] = sum([r.amount_residual_currency for r in self]) + + # Writeoff line in the account of self + first_line_dict = vals.copy() + first_line_dict['account_id'] = self[0].account_id.id + first_line_dict['partner_id'] = self[0].partner_id.id + if 'analytic_account_id' in vals: + del vals['analytic_account_id'] + + # Writeoff line in specified writeoff account + second_line_dict = vals.copy() + second_line_dict['debit'], second_line_dict['credit'] = second_line_dict['credit'], second_line_dict['debit'] + if 'amount_currency' in vals: + second_line_dict['amount_currency'] = -second_line_dict['amount_currency'] + + # Create the move + writeoff_move = self.env['account.move'].create({ + 'journal_id': vals['journal_id'], + 'date': vals['date'], + 'state': 'draft', + 'line_ids': [(0, 0, first_line_dict), (0, 0, second_line_dict)], + }) + writeoff_move.post() + + # Return the writeoff move.line which is to be reconciled + return writeoff_move.line_ids.filtered(lambda r: r.account_id == self[0].account_id) + + @api.multi + def remove_move_reconcile(self): + """ Undo a reconciliation """ + if not self: + return True + rec_move_ids = self.env['account.partial.reconcile'] + for account_move_line in self: + rec_move_ids += account_move_line.matched_debit_ids + rec_move_ids += account_move_line.matched_credit_ids + return rec_move_ids.unlink() + + #################################################### + # CRUD methods + #################################################### + + #TODO: to check/refactor + @api.model + def create(self, vals, apply_taxes=True): + """ :param apply_taxes: set to False if you don't want vals['tax_ids'] to result in the creation of move lines for taxes and eventual + adjustment of the line amount (in case of a tax included in price). This is useful for use cases where you don't want to + apply taxes in the default fashion (eg. taxes). You can also pass 'dont_create_taxes' in context. + + :context's key `check_move_validity`: check data consistency after move line creation. Eg. set to false to disable verification that the move + debit-credit == 0 while creating the move lines composing the move. + + """ + AccountObj = self.env['account.account'] + MoveObj = self.env['account.move'] + context = dict(self._context or {}) + amount = vals.get('debit', 0.0) - vals.get('credit', 0.0) + + if vals.get('move_id', False): + move = MoveObj.browse(vals['move_id']) + if move.date and not vals.get('date'): + vals['date'] = move.date + if ('account_id' in vals) and AccountObj.browse(vals['account_id']).deprecated: + raise UserError(_('You cannot use deprecated account.')) + if 'journal_id' in vals and vals['journal_id']: + context['journal_id'] = vals['journal_id'] + if 'date' in vals and vals['date']: + context['date'] = vals['date'] + if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']: + m = MoveObj.browse(vals['move_id']) + context['journal_id'] = m.journal_id.id + context['date'] = m.date + #we need to treat the case where a value is given in the context for period_id as a string + if not context.get('journal_id', False) and context.get('search_default_journal_id', False): + context['journal_id'] = context.get('search_default_journal_id') + if 'date' not in context: + context['date'] = fields.Date.context_today(self) + move_id = vals.get('move_id', False) + journal = self.env['account.journal'].browse(context['journal_id']) + vals['journal_id'] = vals.get('journal_id') or context.get('journal_id') + vals['date'] = vals.get('date') or context.get('date') + if not move_id: + if not vals.get('move_id', False): + if journal.sequence_id: + #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id) + v = { + 'date': vals.get('date', time.strftime('%Y-%m-%d')), + 'journal_id': context['journal_id'] + } + if vals.get('ref', ''): + v.update({'ref': vals['ref']}) + move_id = MoveObj.with_context(context).create(v) + vals['move_id'] = move_id.id + else: + raise UserError(_('Cannot create an automatic sequence for this piece.\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.')) + ok = not (journal.type_control_ids or journal.account_control_ids) + if ('account_id' in vals): + account = AccountObj.browse(vals['account_id']) + if journal.type_control_ids: + type = account.user_type + for t in journal.type_control_ids: + if type.code == t.code: + ok = True + break + if journal.account_control_ids and not ok: + for a in journal.account_control_ids: + if a.id == vals['account_id']: + ok = True + break + # Automatically convert in the account's secondary currency if there is one and + # the provided values were not already multi-currency + if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id: + vals['currency_id'] = account.currency_id.id + ctx = {} + if 'date' in vals: + ctx['date'] = vals['date'] + vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id) + if not ok: + raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.')) + + # Create tax lines + tax_lines_vals = [] + if apply_taxes and not context.get('dont_create_taxes') and vals.get('tax_ids'): + # Get ids from triplets : https://www.odoo.com/documentation/master/reference/orm.html#openerp.models.Model.write + tax_ids = map(lambda tax: tax[1], vals['tax_ids']) + # Since create() receives ids instead of recordset, let's just use the old-api bridge + taxes = self.env['account.tax'].browse(tax_ids) + res = taxes.compute_all(amount, + vals.get('currency_id'), 1, vals.get('product_id'), vals.get('partner_id')) + # Adjust line amount if any tax is price_include + if abs(res['total_excluded']) < abs(amount): + if vals['debit'] != 0.0: vals['debit'] = res['total_excluded'] + if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded'] + if vals.get('amount_currency'): + vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (amount / res['total_excluded'])) + # Create tax lines + for tax_vals in res['taxes']: + account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id']) + if not account_id: account_id = vals['account_id'] + tax_lines_vals.append({ + 'account_id': account_id, + 'name': vals['name'] + ' ' + tax_vals['name'], + 'tax_line_id': tax_vals['id'], + 'move_id': vals['move_id'], + 'date': vals['date'], + 'partner_id': vals.get('partner_id'), + 'ref': vals.get('ref'), + 'statement_id': vals.get('statement_id'), + 'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0, + 'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0, + }) + + new_line = super(AccountMoveLine, self).create(vals) + for tax_line_vals in tax_lines_vals: + # TODO: remove .with_context(context) once this context nonsense is solved + self.with_context(context).create(tax_line_vals) + + if self._context.get('check_move_validity', True): + move = MoveObj.browse(vals['move_id']) + move.with_context(context)._post_validate() + + return new_line + + @api.multi + def unlink(self): + self._update_check() + move_ids = set() + for line in self: + if line.move_id.id not in move_ids: + move_ids.add(line.move_id.id) + result = super(AccountMoveLine, self).unlink() + if self._context.get('check_move_validity', True) and move_ids: + self.env['account.move'].browse(list(move_ids))._post_validate() + return result + + @api.multi + def write(self, vals): + if vals.get('tax_line_id') or vals.get('tax_ids'): + raise UserError(_('You cannot change the tax, you should remove and recreate lines.')) + if ('account_id' in vals) and self.env['account.account'].browse(vals['account_id']).deprecated: + raise UserError(_('You cannot use deprecated account.')) + if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit', 'amount_currency', 'currency_id')): + self._update_check() + #when we set the expected payment date, log a note on the invoice related (if any) + if vals.get('expected_pay_date') and self.invoice: + msg = _('New expected payment date: ') + vals['expected_pay_date'] + '.\n' + vals.get('internal_note', '') + self.invoice.message_post(body=msg) #TODO: check it is an internal note (not a regular email)! + #when making a reconciliation on an existing liquidity journal item, mark the payment as reconciled + if 'statement_id' in vals and self.payment_id: + # In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement + if all(line.statement_id for line in self.payment_id.move_line_ids.filtered(lambda r: r.id != self.id and r.account_id.internal_type=='liquidity')): + self.payment_id.state = 'reconciled' + + result = super(AccountMoveLine, self).write(vals) + if self._context.get('check_move_validity', True): + move_ids = set() + for line in self: + if line.move_id.id not in move_ids: + move_ids.add(line.move_id.id) + self.env['account.move'].browse(list(move_ids))._post_validate() + return result + + @api.multi + def _update_check(self): + """ Raise Warning to cause rollback if the move is posted, some entries are reconciled or the move is older than the lock date""" + move_ids = set() + for line in self: + err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id)) + if line.move_id.state != 'draft': + raise UserError(_('You cannot do this modification on a posted journal entry, you can just change some non legal fields. You must revert the journal entry to cancel it.\n%s.') % err_msg) + if line.reconciled: + raise UserError(_('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg) + if line.move_id.id not in move_ids: + move_ids.add(line.move_id.id) + self.env['account.move'].browse(list(move_ids))._check_lock_date() + return True + + #################################################### + # Misc / utility methods + #################################################### + + @api.multi + @api.depends('ref', 'move_id') + def name_get(self): + result = [] + for line in self: + if line.ref: + result.append((line.id, (line.move_id.name or '') + '(' + line.ref + ')')) + else: + result.append((line.id, line.move_id.name)) + return result + + @api.model + def compute_amount_fields(self, amount, src_currency, company_currency): + """ Helper function to compute value for fields debit/credit/amount_currency based on an amount and the currencies given in parameter""" + amount_currency = False + if src_currency and src_currency != company_currency: + amount_currency = amount + amount = src_currency.with_context(self._context).compute(amount, company_currency) + debit = amount > 0 and amount or 0.0 + credit = amount < 0 and -amount or 0.0 + return debit, credit, amount_currency + + @api.multi + def create_analytic_lines(self): + """ Create analytic items upon validation of an account.move.line having an analytic account. This + method first remove any existing analytic item related to the line before creating any new one. + """ + for obj_line in self: + if obj_line.analytic_account_id: + if not obj_line.journal_id.analytic_journal_id: + raise UserError(_("You have to define an analytic journal on the '%s' journal!") % (obj_line.journal_id.name, )) + if obj_line.analytic_lines: + obj_line.analytic_lines.unlink() + vals_line = obj_line._prepare_analytic_line()[0] + self.env['account.analytic.line'].create(vals_line) + + @api.one + def _prepare_analytic_line(self): + """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having + an analytic account. This method is intended to be extended in other modules. + """ + return { + 'name': self.name, + 'date': self.date, + 'account_id': self.analytic_account_id.id, + 'unit_amount': self.quantity, + 'product_id': self.product_id and self.product_id.id or False, + 'product_uom_id': self.product_uom_id and self.product_uom_id.id or False, + 'amount': (self.credit or 0.0) - (self.debit or 0.0), + 'general_account_id': self.account_id.id, + 'journal_id': self.journal_id.analytic_journal_id.id, + 'ref': self.ref, + 'move_id': self.id, + 'user_id': self._uid, + } + + @api.model + def _query_get(self, domain=None): + context = dict(self._context or {}) + domain = domain and safe_eval(domain) or [] + + date_field = 'date' + if context.get('aged_balance'): + date_field = 'date_maturity' + if context.get('date_to'): + domain += [(date_field, '<=', context['date_to'])] + if context.get('date_from'): + if not context.get('strict_range'): + domain += ['|', (date_field, '>=', context['date_from']), ('account_id.user_type.include_initial_balance', '=', True)] + else: + domain += [(date_field, '>=', context['date_from'])] + + if context.get('journal_ids'): + domain += [('journal_id', 'in', context['journal_ids'])] + + state = context.get('state') + if state and state.lower() != 'all': + domain += [('move_id.state', '=', state)] + + if context.get('company_id'): + domain += [('company_id', '=', context['company_id'])] + + where_clause = "" + where_clause_params = [] + if domain: + query = self._where_calc(domain) + dummy, where_clause, where_clause_params = query.get_sql() + return where_clause, where_clause_params + + +class AccountPartialReconcile(models.Model): + _name = "account.partial.reconcile" + _description = "Partial Reconcile" + + debit_move_id = fields.Many2one('account.move.line') + credit_move_id = fields.Many2one('account.move.line') + amount = fields.Monetary(currency_field='company_currency_id', help="Amount concerned by this matching. Assumed to be always positive") + amount_currency = fields.Monetary(string="Amount in Currency") + currency_id = fields.Many2one('res.currency', string='Currency') + company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, + help='Utility field to express amount currency') + company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency') + + def create_exchange_rate_entry(self): + """ Automatically create a journal entry to book the exchange rate difference between the `debit_move_id` + and the `credit_move_id`, if both share the same currency, and at the prorata of the amount matched + between them. + That new journal entry is made in the company `currency_exchange_journal_id` and one of its journal + items is matched with the other lines to ensure having the residual amount in company currency that + reflects perfectly the residual amount in foreign currency. + """ + for rec in self: + if rec.currency_id and rec.debit_move_id.amount_currency and rec.credit_move_id.amount_currency: + #create exchange rate difference journal entry + rate_diff = rec.debit_move_id.debit / rec.debit_move_id.amount_currency - rec.credit_move_id.credit / -rec.credit_move_id.amount_currency + if rec.amount_currency and rec.company_id.currency_id.round(rec.amount_currency * rate_diff): + if not rec.company_id.currency_exchange_journal_id: + raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + if not self.company_id.income_currency_exchange_account_id.id: + raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + if not self.company_id.expense_currency_exchange_account_id.id: + raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + amount_diff = rec.company_id.currency_id.round(rec.amount_currency * rate_diff) + move = rec.env['account.move'].create({'journal_id': rec.company_id.currency_exchange_journal_id.id, 'rate_diff_partial_rec_id': rec.id}) + line_to_reconcile = rec.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_diff < 0 and -amount_diff or 0.0, + 'credit': amount_diff > 0 and amount_diff or 0.0, + 'account_id': rec.debit_move_id.account_id.id, + 'move_id': move.id, + 'currency_id': rec.currency_id.id, + }) + rec.env['account.move.line'].create({ + 'name': _('Currency exchange rate difference'), + 'debit': amount_diff > 0 and amount_diff or 0.0, + 'credit': amount_diff < 0 and -amount_diff or 0.0, + 'account_id': amount_diff > 0 and rec.company_id.currency_exchange_journal_id.default_debit_account_id.id or rec.company_id.currency_exchange_journal_id.default_credit_account_id.id, + 'move_id': move.id, + 'currency_id': rec.currency_id.id, + }) + rec.env['account.partial.reconcile'].create({ + 'debit_move_id': amount_diff < 0 and line_to_reconcile.id or rec.debit_move_id.id, + 'credit_move_id': amount_diff > 0 and line_to_reconcile.id or rec.credit_move_id.id, + 'amount': abs(amount_diff), + 'amount_currency': 0.0, + 'currency_id': rec.debit_move_id.currency_id.id, + }) + move.post() + + @api.model + def create(self, vals): + res = super(AccountPartialReconcile, self).create(vals) + #eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates + res.create_exchange_rate_entry() + return res + + @api.multi + def unlink(self): + """ When removing a link between entries, we need to revert the eventual journal entries we created to book the + fluctuation of the foreign currency's exchange rate. + """ + exchange_rate_entries = self.env['account.move'].search([('rate_diff_partial_rec_id', 'in', self.ids)]) + exchange_rate_entries.reverse_moves() + return super(AccountPartialReconcile, self).unlink() diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py deleted file mode 100644 index 9bed6f7120c..00000000000 --- a/addons/account/account_move_line.py +++ /dev/null @@ -1,1429 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 time -from datetime import datetime - -from openerp import workflow -from openerp.osv import fields, osv -from openerp.tools.translate import _ -import openerp.addons.decimal_precision as dp -from openerp import tools -from openerp.report import report_sxw -import openerp -from openerp.exceptions import UserError - -class account_move_line(osv.osv): - _name = "account.move.line" - _description = "Journal Items" - - def _query_get(self, cr, uid, obj='l', context=None): - fiscalyear_obj = self.pool.get('account.fiscalyear') - fiscalperiod_obj = self.pool.get('account.period') - account_obj = self.pool.get('account.account') - fiscalyear_ids = [] - context = dict(context or {}) - initial_bal = context.get('initial_bal', False) - company_clause = " " - if context.get('company_id', False): - company_clause = " AND " +obj+".company_id = %s" % context.get('company_id', False) - if not context.get('fiscalyear', False): - if context.get('all_fiscalyear', False): - #this option is needed by the aged balance report because otherwise, if we search only the draft ones, an open invoice of a closed fiscalyear won't be displayed - fiscalyear_ids = fiscalyear_obj.search(cr, uid, []) - else: - fiscalyear_ids = fiscalyear_obj.search(cr, uid, [('state', '=', 'draft')]) - else: - #for initial balance as well as for normal query, we check only the selected FY because the best practice is to generate the FY opening entries - fiscalyear_ids = [context['fiscalyear']] - - fiscalyear_clause = (','.join([str(x) for x in fiscalyear_ids])) or '0' - state = context.get('state', False) - where_move_state = '' - where_move_lines_by_date = '' - - if context.get('date_from', False) and context.get('date_to', False): - if initial_bal: - where_move_lines_by_date = " AND " +obj+".move_id IN (SELECT id FROM account_move WHERE date < '" +context['date_from']+"')" - else: - where_move_lines_by_date = " AND " +obj+".move_id IN (SELECT id FROM account_move WHERE date >= '" +context['date_from']+"' AND date <= '"+context['date_to']+"')" - - if state: - if state.lower() not in ['all']: - where_move_state= " AND "+obj+".move_id IN (SELECT id FROM account_move WHERE account_move.state = '"+state+"')" - if context.get('period_from', False) and context.get('period_to', False) and not context.get('periods', False): - if initial_bal: - period_company_id = fiscalperiod_obj.browse(cr, uid, context['period_from'], context=context).company_id.id - first_period = fiscalperiod_obj.search(cr, uid, [('company_id', '=', period_company_id)], order='date_start', limit=1)[0] - context['periods'] = fiscalperiod_obj.build_ctx_periods(cr, uid, first_period, context['period_from']) - else: - context['periods'] = fiscalperiod_obj.build_ctx_periods(cr, uid, context['period_from'], context['period_to']) - if context.get('periods', False): - if initial_bal: - query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s)) %s %s" % (fiscalyear_clause, where_move_state, where_move_lines_by_date) - period_ids = fiscalperiod_obj.search(cr, uid, [('id', 'in', context['periods'])], order='date_start', limit=1) - if period_ids and period_ids[0]: - first_period = fiscalperiod_obj.browse(cr, uid, period_ids[0], context=context) - ids = ','.join([str(x) for x in context['periods']]) - query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s) AND date_start <= '%s' AND id NOT IN (%s)) %s %s" % (fiscalyear_clause, first_period.date_start, ids, where_move_state, where_move_lines_by_date) - else: - ids = ','.join([str(x) for x in context['periods']]) - query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s) AND id IN (%s)) %s %s" % (fiscalyear_clause, ids, where_move_state, where_move_lines_by_date) - else: - query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s)) %s %s" % (fiscalyear_clause, where_move_state, where_move_lines_by_date) - - if initial_bal and not context.get('periods', False) and not where_move_lines_by_date: - #we didn't pass any filter in the context, and the initial balance can't be computed using only the fiscalyear otherwise entries will be summed twice - #so we have to invalidate this query - raise UserError(("You have not supplied enough arguments to compute the initial balance, please select a period and a journal in the context.")) - - - if context.get('journal_ids', False): - query += ' AND '+obj+'.journal_id IN (%s)' % ','.join(map(str, context['journal_ids'])) - - if context.get('chart_account_id', False): - child_ids = account_obj._get_children_and_consol(cr, uid, [context['chart_account_id']], context=context) - query += ' AND '+obj+'.account_id IN (%s)' % ','.join(map(str, child_ids)) - - query += company_clause - return query - - def _amount_residual(self, cr, uid, ids, field_names, args, context=None): - """ - This function returns the residual amount on a receivable or payable account.move.line. - By default, it returns an amount in the currency of this journal entry (maybe different - of the company currency), but if you pass 'residual_in_company_currency' = True in the - context then the returned amount will be in company currency. - """ - res = {} - if context is None: - context = {} - cur_obj = self.pool.get('res.currency') - for move_line in self.browse(cr, uid, ids, context=context): - res[move_line.id] = { - 'amount_residual': 0.0, - 'amount_residual_currency': 0.0, - } - - if move_line.reconcile_id: - continue - if not move_line.account_id.reconcile: - #this function does not suport to be used on move lines not related to a reconcilable account - continue - - if move_line.currency_id: - move_line_total = move_line.amount_currency - sign = move_line.amount_currency < 0 and -1 or 1 - else: - move_line_total = move_line.debit - move_line.credit - sign = (move_line.debit - move_line.credit) < 0 and -1 or 1 - line_total_in_company_currency = move_line.debit - move_line.credit - context_unreconciled = context.copy() - if move_line.reconcile_partial_id: - for payment_line in move_line.reconcile_partial_id.line_partial_ids: - if payment_line.id == move_line.id: - continue - if payment_line.currency_id and move_line.currency_id and payment_line.currency_id.id == move_line.currency_id.id: - move_line_total += payment_line.amount_currency - else: - if move_line.currency_id: - context_unreconciled.update({'date': payment_line.date}) - amount_in_foreign_currency = cur_obj.compute(cr, uid, move_line.company_id.currency_id.id, move_line.currency_id.id, (payment_line.debit - payment_line.credit), round=False, context=context_unreconciled) - move_line_total += amount_in_foreign_currency - else: - move_line_total += (payment_line.debit - payment_line.credit) - line_total_in_company_currency += (payment_line.debit - payment_line.credit) - - result = move_line_total - res[move_line.id]['amount_residual_currency'] = sign * (move_line.currency_id and self.pool.get('res.currency').round(cr, uid, move_line.currency_id, result) or result) - res[move_line.id]['amount_residual'] = sign * line_total_in_company_currency - return res - - def default_get(self, cr, uid, fields, context=None): - data = self._default_get(cr, uid, fields, context=context) - for f in data.keys(): - if f not in fields: - del data[f] - return data - - def _prepare_analytic_line(self, cr, uid, obj_line, context=None): - """ - Prepare the values given at the create() of account.analytic.line upon the validation of a journal item having - an analytic account. This method is intended to be extended in other modules. - - :param obj_line: browse record of the account.move.line that triggered the analytic line creation - """ - return {'name': obj_line.name, - 'date': obj_line.date, - 'account_id': obj_line.analytic_account_id.id, - 'unit_amount': obj_line.quantity, - 'product_id': obj_line.product_id and obj_line.product_id.id or False, - 'product_uom_id': obj_line.product_uom_id and obj_line.product_uom_id.id or False, - 'amount': (obj_line.credit or 0.0) - (obj_line.debit or 0.0), - 'general_account_id': obj_line.account_id.id, - 'journal_id': obj_line.journal_id.analytic_journal_id.id, - 'ref': obj_line.ref, - 'move_id': obj_line.id, - 'user_id': uid, - } - - def create_analytic_lines(self, cr, uid, ids, context=None): - acc_ana_line_obj = self.pool.get('account.analytic.line') - for obj_line in self.browse(cr, uid, ids, context=context): - if obj_line.analytic_lines: - acc_ana_line_obj.unlink(cr,uid,[obj.id for obj in obj_line.analytic_lines]) - if obj_line.analytic_account_id: - if not obj_line.journal_id.analytic_journal_id: - raise UserError(("You have to define an analytic journal on the '%s' journal!") % (obj_line.journal_id.name, )) - vals_line = self._prepare_analytic_line(cr, uid, obj_line, context=context) - acc_ana_line_obj.create(cr, uid, vals_line) - return True - - def _default_get_move_form_hook(self, cursor, user, data): - '''Called in the end of default_get method for manual entry in account_move form''' - if data.has_key('analytic_account_id'): - del(data['analytic_account_id']) - if data.has_key('account_tax_id'): - del(data['account_tax_id']) - return data - - def convert_to_period(self, cr, uid, context=None): - if context is None: - context = {} - period_obj = self.pool.get('account.period') - #check if the period_id changed in the context from client side - if context.get('period_id', False): - period_id = context.get('period_id') - if type(period_id) == str: - ids = period_obj.search(cr, uid, [('name', 'ilike', period_id)]) - context = dict(context, period_id=ids and ids[0] or False) - return context - - def _default_get(self, cr, uid, fields, context=None): - #default_get should only do the following: - # -propose the next amount in debit/credit in order to balance the move - # -propose the next account from the journal (default debit/credit account) accordingly - context = dict(context or {}) - account_obj = self.pool.get('account.account') - period_obj = self.pool.get('account.period') - journal_obj = self.pool.get('account.journal') - move_obj = self.pool.get('account.move') - tax_obj = self.pool.get('account.tax') - fiscal_pos_obj = self.pool.get('account.fiscal.position') - partner_obj = self.pool.get('res.partner') - currency_obj = self.pool.get('res.currency') - - if not context.get('journal_id', False): - context['journal_id'] = context.get('search_default_journal_id', False) - if not context.get('period_id', False): - context['period_id'] = context.get('search_default_period_id', False) - context = self.convert_to_period(cr, uid, context) - - # Compute simple values - data = super(account_move_line, self).default_get(cr, uid, fields, context=context) - if context.get('journal_id'): - total = 0.0 - #in account.move form view, it is not possible to compute total debit and credit using - #a browse record. So we must use the context to pass the whole one2many field and compute the total - if context.get('line_id'): - for move_line_dict in move_obj.resolve_2many_commands(cr, uid, 'line_id', context.get('line_id'), context=context): - data['name'] = data.get('name') or move_line_dict.get('name') - data['partner_id'] = data.get('partner_id') or move_line_dict.get('partner_id') - total += move_line_dict.get('debit', 0.0) - move_line_dict.get('credit', 0.0) - elif context.get('period_id'): - #find the date and the ID of the last unbalanced account.move encoded by the current user in that journal and period - move_id = False - cr.execute('''SELECT move_id, date FROM account_move_line - WHERE journal_id = %s AND period_id = %s AND create_uid = %s AND state = %s - ORDER BY id DESC limit 1''', (context['journal_id'], context['period_id'], uid, 'draft')) - res = cr.fetchone() - move_id = res and res[0] or False - data['date'] = res and res[1] or period_obj.browse(cr, uid, context['period_id'], context=context).date_start - data['move_id'] = move_id - if move_id: - #if there exist some unbalanced accounting entries that match the journal and the period, - #we propose to continue the same move by copying the ref, the name, the partner... - move = move_obj.browse(cr, uid, move_id, context=context) - data.setdefault('name', move.line_id[-1].name) - for l in move.line_id: - data['partner_id'] = data.get('partner_id') or l.partner_id.id - data['ref'] = data.get('ref') or l.ref - total += (l.debit or 0.0) - (l.credit or 0.0) - - #compute the total of current move - data['debit'] = total < 0 and -total or 0.0 - data['credit'] = total > 0 and total or 0.0 - #pick the good account on the journal accordingly if the next proposed line will be a debit or a credit - journal_data = journal_obj.browse(cr, uid, context['journal_id'], context=context) - account = total > 0 and journal_data.default_credit_account_id or journal_data.default_debit_account_id - #map the account using the fiscal position of the partner, if needed - if isinstance(data.get('partner_id'), (int, long)): - part = partner_obj.browse(cr, uid, data['partner_id'], context=context) - elif isinstance(data.get('partner_id'), (tuple, list)): - part = partner_obj.browse(cr, uid, data['partner_id'][0], context=context) - else: - part = False - if account and part: - account = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, account.id) - account = account_obj.browse(cr, uid, account, context=context) - data['account_id'] = account and account.id or False - #compute the amount in secondary currency of the account, if needed - if account and account.currency_id: - data['currency_id'] = account.currency_id.id - #set the context for the multi currency change - compute_ctx = context.copy() - compute_ctx.update({ - #the following 2 parameters are used to choose the currency rate, in case where the account - #doesn't work with an outgoing currency rate method 'at date' but 'average' - 'res.currency.compute.account': account, - 'res.currency.compute.account_invert': True, - }) - if data.get('date'): - compute_ctx.update({'date': data['date']}) - data['amount_currency'] = currency_obj.compute(cr, uid, account.company_id.currency_id.id, data['currency_id'], -total, context=compute_ctx) - data = self._default_get_move_form_hook(cr, uid, data) - return data - - def on_create_write(self, cr, uid, id, context=None): - if not id: - return [] - ml = self.browse(cr, uid, id, context=context) - domain = (context or {}).get('on_write_domain', []) - return self.pool.get('account.move.line').search(cr, uid, domain + [['id', 'in', [l.id for l in ml.move_id.line_id]]], context=context) - - def _balance(self, cr, uid, ids, name, arg, context=None): - if context is None: - context = {} - c = context.copy() - c['initital_bal'] = True - sql = """SELECT l1.id, COALESCE(SUM(l2.debit-l2.credit), 0) - FROM account_move_line l1 LEFT JOIN account_move_line l2 - ON (l1.account_id = l2.account_id - AND l2.id <= l1.id - AND """ + \ - self._query_get(cr, uid, obj='l2', context=c) + \ - ") WHERE l1.id IN %s GROUP BY l1.id" - - cr.execute(sql, [tuple(ids)]) - return dict(cr.fetchall()) - - def _invoice(self, cursor, user, ids, name, arg, context=None): - invoice_obj = self.pool.get('account.invoice') - res = {} - for line_id in ids: - res[line_id] = False - cursor.execute('SELECT l.id, i.id ' \ - 'FROM account_move_line l, account_invoice i ' \ - 'WHERE l.move_id = i.move_id ' \ - 'AND l.id IN %s', - (tuple(ids),)) - invoice_ids = [] - for line_id, invoice_id in cursor.fetchall(): - res[line_id] = invoice_id - invoice_ids.append(invoice_id) - invoice_names = {} - for invoice_id, name in invoice_obj.name_get(cursor, user, invoice_ids, context=context): - invoice_names[invoice_id] = name - for line_id in res.keys(): - invoice_id = res[line_id] - res[line_id] = invoice_id and (invoice_id, invoice_names[invoice_id]) or False - return res - - def name_get(self, cr, uid, ids, context=None): - if not ids: - return [] - result = [] - for line in self.browse(cr, uid, ids, context=context): - if line.ref: - result.append((line.id, (line.move_id.name or '')+' ('+line.ref+')')) - else: - result.append((line.id, line.move_id.name)) - return result - - def _balance_search(self, cursor, user, obj, name, args, domain=None, context=None): - if context is None: - context = {} - if not args: - return [] - where = ' AND '.join(map(lambda x: '(abs(sum(debit-credit))'+x[1]+str(x[2])+')',args)) - cursor.execute('SELECT id, SUM(debit-credit) FROM account_move_line \ - GROUP BY id, debit, credit having '+where) - res = cursor.fetchall() - if not res: - return [('id', '=', '0')] - return [('id', 'in', [x[0] for x in res])] - - def _invoice_search(self, cursor, user, obj, name, args, context=None): - if not args: - return [] - invoice_obj = self.pool.get('account.invoice') - i = 0 - while i < len(args): - fargs = args[i][0].split('.', 1) - if len(fargs) > 1: - args[i] = (fargs[0], 'in', invoice_obj.search(cursor, user, - [(fargs[1], args[i][1], args[i][2])])) - i += 1 - continue - if isinstance(args[i][2], basestring): - res_ids = invoice_obj.name_search(cursor, user, args[i][2], [], - args[i][1]) - args[i] = (args[i][0], 'in', [x[0] for x in res_ids]) - i += 1 - qu1, qu2 = [], [] - for x in args: - if x[1] != 'in': - if (x[2] is False) and (x[1] == '='): - qu1.append('(i.id IS NULL)') - elif (x[2] is False) and (x[1] == '<>' or x[1] == '!='): - qu1.append('(i.id IS NOT NULL)') - else: - qu1.append('(i.id %s %s)' % (x[1], '%s')) - qu2.append(x[2]) - elif x[1] == 'in': - if len(x[2]) > 0: - qu1.append('(i.id IN (%s))' % (','.join(['%s'] * len(x[2])))) - qu2 += x[2] - else: - qu1.append(' (False)') - if qu1: - qu1 = ' AND' + ' AND'.join(qu1) - else: - qu1 = '' - cursor.execute('SELECT l.id ' \ - 'FROM account_move_line l, account_invoice i ' \ - 'WHERE l.move_id = i.move_id ' + qu1, qu2) - res = cursor.fetchall() - if not res: - return [('id', '=', '0')] - return [('id', 'in', [x[0] for x in res])] - - def _get_move_lines(self, cr, uid, ids, context=None): - result = [] - for move in self.pool.get('account.move').browse(cr, uid, ids, context=context): - for line in move.line_id: - result.append(line.id) - return result - - def _get_reconcile(self, cr, uid, ids,name, unknow_none, context=None): - res = dict.fromkeys(ids, False) - for line in self.browse(cr, uid, ids, context=context): - if line.reconcile_id: - res[line.id] = str(line.reconcile_id.name) - elif line.reconcile_partial_id: - res[line.id] = str(line.reconcile_partial_id.name) - return res - - def _get_move_from_reconcile(self, cr, uid, ids, context=None): - move = {} - for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context): - for line in r.line_partial_ids: - move[line.move_id.id] = True - for line in r.line_id: - move[line.move_id.id] = True - move_line_ids = [] - if move: - move_line_ids = self.pool.get('account.move.line').search(cr, uid, [('move_id','in',move.keys())], context=context) - return move_line_ids - - - _columns = { - 'name': fields.char('Name', required=True), - 'quantity': fields.float('Quantity', digits=(16,2), help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports."), - 'product_uom_id': fields.many2one('product.uom', 'Unit of Measure'), - 'product_id': fields.many2one('product.product', 'Product'), - 'debit': fields.float('Debit', digits_compute=dp.get_precision('Account')), - 'credit': fields.float('Credit', digits_compute=dp.get_precision('Account')), - 'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade", domain=[('type','<>','view'), ('type', '<>', 'closed')], select=2), - 'move_id': fields.many2one('account.move', 'Journal Entry', ondelete="cascade", help="The move of this entry line.", select=2, required=True, auto_join=True), - 'narration': fields.related('move_id','narration', type='text', relation='account.move', string='Internal Note'), - 'ref': fields.related('move_id', 'ref', string='Reference', type='char', store=True), - 'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=1, copy=False), - 'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=2, copy=False), - 'reconcile_partial_id': fields.many2one('account.move.reconcile', 'Partial Reconcile', readonly=True, ondelete='set null', select=2, copy=False), - 'reconcile_ref': fields.function(_get_reconcile, type='char', string='Reconcile Ref', oldname='reconcile', store={ - 'account.move.line': (lambda self, cr, uid, ids, c={}: ids, ['reconcile_id','reconcile_partial_id'], 50),'account.move.reconcile': (_get_move_from_reconcile, None, 50)}), - 'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')), - 'amount_residual_currency': fields.function(_amount_residual, string='Residual Amount in Currency', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in its currency (maybe different of the company currency)."), - 'amount_residual': fields.function(_amount_residual, string='Residual Amount', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in the company currency."), - 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."), - 'journal_id': fields.related('move_id', 'journal_id', string='Journal', type='many2one', relation='account.journal', required=True, select=True, - store = { - 'account.move': (_get_move_lines, ['journal_id'], 20) - }), - 'period_id': fields.related('move_id', 'period_id', string='Period', type='many2one', relation='account.period', required=True, select=True, - store = { - 'account.move': (_get_move_lines, ['period_id'], 20) - }), - 'blocked': fields.boolean('No Follow-up', help="You can check this box to mark this journal item as a litigation with the associated partner"), - 'partner_id': fields.many2one('res.partner', 'Partner', select=1, ondelete='restrict'), - 'date_maturity': fields.date('Due date', select=True ,help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line."), - 'date': fields.related('move_id','date', string='Effective date', type='date', required=True, select=True, - store = { - 'account.move': (_get_move_lines, ['date'], 20) - }), - 'date_created': fields.date('Creation date', select=True), - 'analytic_lines': fields.one2many('account.analytic.line', 'move_id', 'Analytic lines'), - 'centralisation': fields.selection([('normal','Normal'),('credit','Credit Centralisation'),('debit','Debit Centralisation'),('currency','Currency Adjustment')], 'Centralisation', size=8), - 'balance': fields.function(_balance, fnct_search=_balance_search, string='Balance'), - 'state': fields.selection([('draft','Unbalanced'), ('valid','Balanced')], 'Status', readonly=True, copy=False), - 'tax_code_id': fields.many2one('account.tax.code', 'Tax Account', help="The Account can either be a base tax code or a tax code account."), - 'tax_amount': fields.float('Tax/Base Amount', digits_compute=dp.get_precision('Account'), select=True, help="If the Tax account is a tax code account, this field will contain the taxed amount.If the tax account is base tax code, "\ - "this field will contain the basic amount(without tax)."), - 'invoice': fields.function(_invoice, string='Invoice', - type='many2one', relation='account.invoice', fnct_search=_invoice_search), - 'account_tax_id':fields.many2one('account.tax', 'Tax', copy=False), - 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'), - 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', - string='Company', store=True, readonly=True) - } - - def _get_date(self, cr, uid, context=None): - if context is None: - context or {} - period_obj = self.pool.get('account.period') - dt = time.strftime('%Y-%m-%d') - if context.get('journal_id') and context.get('period_id'): - cr.execute('SELECT date FROM account_move_line ' \ - 'WHERE journal_id = %s AND period_id = %s ' \ - 'ORDER BY id DESC limit 1', - (context['journal_id'], context['period_id'])) - res = cr.fetchone() - if res: - dt = res[0] - else: - period = period_obj.browse(cr, uid, context['period_id'], context=context) - dt = period.date_start - return dt - - def _get_currency(self, cr, uid, context=None): - if context is None: - context = {} - if not context.get('journal_id', False): - return False - cur = self.pool.get('account.journal').browse(cr, uid, context['journal_id']).currency - return cur and cur.id or False - - def _get_period(self, cr, uid, context=None): - """ - Return default account period value - """ - context = context or {} - if context.get('period_id', False): - return context['period_id'] - account_period_obj = self.pool.get('account.period') - ids = account_period_obj.find(cr, uid, context=context) - period_id = False - if ids: - period_id = ids[0] - return period_id - - def _get_journal(self, cr, uid, context=None): - """ - Return journal based on the journal type - """ - context = context or {} - if context.get('journal_id', False): - return context['journal_id'] - journal_id = False - - journal_pool = self.pool.get('account.journal') - if context.get('journal_type', False): - jids = journal_pool.search(cr, uid, [('type','=', context.get('journal_type'))]) - if not jids: - model, action_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'account', 'action_account_journal_form') - msg = _("""Cannot find any account journal of "%s" type for this company, You should create one.\n Please go to Journal Configuration""") % context.get('journal_type').replace('_', ' ').title() - raise openerp.exceptions.RedirectWarning(msg, action_id, _('Go to the configuration panel')) - journal_id = jids[0] - return journal_id - - - _defaults = { - 'blocked': False, - 'centralisation': 'normal', - 'date': _get_date, - 'date_created': fields.date.context_today, - 'state': 'draft', - 'currency_id': _get_currency, - 'journal_id': _get_journal, - 'credit': 0.0, - 'debit': 0.0, - 'amount_currency': 0.0, - 'account_id': lambda self, cr, uid, c: c.get('account_id', False), - 'period_id': _get_period, - 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.move.line', context=c) - } - _order = "date desc, id desc" - _sql_constraints = [ - ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'), - ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'), - ] - - def _auto_init(self, cr, context=None): - res = super(account_move_line, self)._auto_init(cr, context=context) - cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'account_move_line_journal_id_period_id_index\'') - if not cr.fetchone(): - cr.execute('CREATE INDEX account_move_line_journal_id_period_id_index ON account_move_line (journal_id, period_id)') - cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('account_move_line_date_id_index',)) - if not cr.fetchone(): - cr.execute('CREATE INDEX account_move_line_date_id_index ON account_move_line (date DESC, id desc)') - return res - - def _check_no_view(self, cr, uid, ids, context=None): - lines = self.browse(cr, uid, ids, context=context) - for l in lines: - if l.account_id.type in ('view', 'consolidation'): - return False - return True - - def _check_no_closed(self, cr, uid, ids, context=None): - lines = self.browse(cr, uid, ids, context=context) - for l in lines: - if l.account_id.type == 'closed': - raise UserError(('You cannot create journal items on a closed account %s %s.') % (l.account_id.code, l.account_id.name)) - return True - - def _check_company_id(self, cr, uid, ids, context=None): - lines = self.browse(cr, uid, ids, context=context) - for l in lines: - if l.company_id != l.account_id.company_id or l.company_id != l.period_id.company_id: - return False - return True - - def _check_date(self, cr, uid, ids, context=None): - for l in self.browse(cr, uid, ids, context=context): - if l.journal_id.allow_date: - if not time.strptime(l.date[:10],'%Y-%m-%d') >= time.strptime(l.period_id.date_start, '%Y-%m-%d') or not time.strptime(l.date[:10], '%Y-%m-%d') <= time.strptime(l.period_id.date_stop, '%Y-%m-%d'): - return False - return True - - def _check_currency(self, cr, uid, ids, context=None): - for l in self.browse(cr, uid, ids, context=context): - if l.account_id.currency_id: - if not l.currency_id or not l.currency_id.id == l.account_id.currency_id.id: - return False - return True - - def _check_currency_and_amount(self, cr, uid, ids, context=None): - for l in self.browse(cr, uid, ids, context=context): - if (l.amount_currency and not l.currency_id): - return False - return True - - def _check_currency_amount(self, cr, uid, ids, context=None): - for l in self.browse(cr, uid, ids, context=context): - if l.amount_currency: - if (l.amount_currency > 0.0 and l.credit > 0.0) or (l.amount_currency < 0.0 and l.debit > 0.0): - return False - return True - - def _check_currency_company(self, cr, uid, ids, context=None): - for l in self.browse(cr, uid, ids, context=context): - if l.currency_id.id == l.company_id.currency_id.id: - return False - return True - - _constraints = [ - (_check_no_view, 'You cannot create journal items on an account of type view or consolidation.', ['account_id']), - (_check_no_closed, 'You cannot create journal items on closed account.', ['account_id']), - (_check_company_id, 'Account and Period must belong to the same company.', ['company_id']), - (_check_date, 'The date of your Journal Entry is not in the defined period! You should change the date or remove this constraint from the journal.', ['date']), - (_check_currency, 'The selected account of your Journal Entry forces to provide a secondary currency. You should remove the secondary currency on the account or select a multi-currency view on the journal.', ['currency_id']), - (_check_currency_and_amount, "You cannot create journal items with a secondary currency without recording both 'currency' and 'amount currency' field.", ['currency_id','amount_currency']), - (_check_currency_amount, 'The amount expressed in the secondary currency must be positive when account is debited and negative when account is credited.', ['amount_currency']), - (_check_currency_company, "You cannot provide a secondary currency if it is the same than the company one." , ['currency_id']), - ] - - #TODO: ONCHANGE_ACCOUNT_ID: set account_tax_id - def onchange_currency(self, cr, uid, ids, account_id, amount, currency_id, date=False, journal=False, context=None): - if context is None: - context = {} - account_obj = self.pool.get('account.account') - journal_obj = self.pool.get('account.journal') - currency_obj = self.pool.get('res.currency') - if (not currency_id) or (not account_id): - return {} - result = {} - acc = account_obj.browse(cr, uid, account_id, context=context) - if (amount>0) and journal: - x = journal_obj.browse(cr, uid, journal).default_credit_account_id - if x: acc = x - context = dict(context) - context.update({ - 'date': date, - 'res.currency.compute.account': acc, - }) - v = currency_obj.compute(cr, uid, currency_id, acc.company_id.currency_id.id, amount, context=context) - result['value'] = { - 'debit': v > 0 and v or 0.0, - 'credit': v < 0 and -v or 0.0 - } - return result - - def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False, context=None): - partner_obj = self.pool.get('res.partner') - payment_term_obj = self.pool.get('account.payment.term') - journal_obj = self.pool.get('account.journal') - fiscal_pos_obj = self.pool.get('account.fiscal.position') - val = {} - val['date_maturity'] = False - - if not partner_id: - return {'value':val} - if not date: - date = datetime.now().strftime('%Y-%m-%d') - jt = False - if journal: - jt = journal_obj.browse(cr, uid, journal, context=context).type - part = partner_obj.browse(cr, uid, partner_id, context=context) - - payment_term_id = False - if jt and jt in ('purchase', 'purchase_refund') and part.property_supplier_payment_term: - payment_term_id = part.property_supplier_payment_term.id - elif jt and part.property_payment_term: - payment_term_id = part.property_payment_term.id - if payment_term_id: - res = payment_term_obj.compute(cr, uid, payment_term_id, 100, date) - if res: - val['date_maturity'] = res[0][0] - if not account_id: - id1 = part.property_account_payable.id - id2 = part.property_account_receivable.id - if jt: - if jt in ('sale', 'purchase_refund'): - val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2) - elif jt in ('purchase', 'sale_refund'): - val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1) - elif jt in ('general', 'bank', 'cash'): - if part.customer: - val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2) - elif part.supplier: - val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1) - if val.get('account_id', False): - d = self.onchange_account_id(cr, uid, ids, account_id=val['account_id'], partner_id=part.id, context=context) - val.update(d['value']) - return {'value':val} - - def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False, context=None): - account_obj = self.pool.get('account.account') - partner_obj = self.pool.get('res.partner') - fiscal_pos_obj = self.pool.get('account.fiscal.position') - val = {} - if account_id: - res = account_obj.browse(cr, uid, account_id, context=context) - tax_ids = res.tax_ids - if tax_ids and partner_id: - part = partner_obj.browse(cr, uid, partner_id, context=context) - tax_id = fiscal_pos_obj.map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0] - else: - tax_id = tax_ids and tax_ids[0].id or False - val['account_tax_id'] = tax_id - return {'value': val} - # - # type: the type if reconciliation (no logic behind this field, for info) - # - # writeoff; entry generated for the difference between the lines - # - def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): - if context is None: - context = {} - if context.get('fiscalyear'): - args.append(('period_id.fiscalyear_id', '=', context.get('fiscalyear', False))) - if context and context.get('next_partner_only', False): - if not context.get('partner_id', False): - partner = self.list_partners_to_reconcile(cr, uid, context=context) - if partner: - partner = partner[0] - else: - partner = context.get('partner_id', False) - if not partner: - return [] - args.append(('partner_id', '=', partner[0])) - return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count) - - def prepare_move_lines_for_reconciliation_widget(self, cr, uid, lines, target_currency=False, target_date=False, context=None): - """ Returns move lines formatted for the bank reconciliation widget - - :param target_currency: curreny you want the move line debit/credit converted into - :param target_date: date to use for the monetary conversion - """ - if not lines: - return [] - if context is None: - context = {} - ctx = context.copy() - currency_obj = self.pool.get('res.currency') - company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id - rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_aml', context=context) - ret = [] - - for line in lines: - partial_reconciliation_siblings_ids = [] - if line.reconcile_partial_id: - partial_reconciliation_siblings_ids = self.search(cr, uid, [('reconcile_partial_id', '=', line.reconcile_partial_id.id)], context=context) - partial_reconciliation_siblings_ids.remove(line.id) - - ret_line = { - 'id': line.id, - 'name': line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name, - 'ref': line.move_id.ref or '', - # For reconciliation between statement transactions and already registered payments (eg. checks) - 'already_paid': line.account_id.type == 'liquidity', - 'account_code': line.account_id.code, - 'account_name': line.account_id.name, - 'account_type': line.account_id.type, - 'date_maturity': line.date_maturity, - 'date': line.date, - 'period_name': line.period_id.name, - 'journal_name': line.journal_id.name, - 'partner_id': line.partner_id.id, - 'partner_name': line.partner_id.name, - 'is_partially_reconciled': bool(line.reconcile_partial_id), - 'partial_reconciliation_siblings_ids': partial_reconciliation_siblings_ids, - } - - # Amount residual can be negative - debit = line.debit - credit = line.credit - amount = line.amount_residual - amount_currency = line.amount_residual_currency - if line.amount_residual < 0: - debit, credit = credit, debit - amount = -amount - amount_currency = -amount_currency - - # For already reconciled lines, don't use amount_residual(_currency) - if line.account_id.type == 'liquidity': - amount = abs(debit - credit) - amount_currency = abs(line.amount_currency) - - # Get right debit / credit: - target_currency = target_currency or company_currency - line_currency = line.currency_id or company_currency - amount_currency_str = "" - total_amount_currency_str = "" - if line_currency != company_currency: - total_amount = line.amount_currency - actual_debit = debit > 0 and amount_currency or 0.0 - actual_credit = credit > 0 and amount_currency or 0.0 - else: - total_amount = abs(debit - credit) - actual_debit = debit > 0 and amount or 0.0 - actual_credit = credit > 0 and amount or 0.0 - if line_currency != target_currency: - amount_currency_str = rml_parser.formatLang(actual_debit or actual_credit, currency_obj=line_currency) - total_amount_currency_str = rml_parser.formatLang(total_amount, currency_obj=line_currency) - ret_line['credit_currency'] = actual_credit - ret_line['debit_currency'] = actual_debit - ctx = context.copy() - if target_date: - ctx.update({'date': target_date}) - total_amount = currency_obj.compute(cr, uid, line_currency.id, target_currency.id, total_amount, context=ctx) - actual_debit = currency_obj.compute(cr, uid, line_currency.id, target_currency.id, actual_debit, context=ctx) - actual_credit = currency_obj.compute(cr, uid, line_currency.id, target_currency.id, actual_credit, context=ctx) - amount_str = rml_parser.formatLang(actual_debit or actual_credit, currency_obj=target_currency) - total_amount_str = rml_parser.formatLang(total_amount, currency_obj=target_currency) - - ret_line['debit'] = actual_debit - ret_line['credit'] = actual_credit - ret_line['amount_str'] = amount_str - ret_line['total_amount_str'] = total_amount_str - ret_line['amount_currency_str'] = amount_currency_str - ret_line['total_amount_currency_str'] = total_amount_currency_str - ret.append(ret_line) - return ret - - - def list_partners_to_reconcile(self, cr, uid, context=None, filter_domain=False): - line_ids = [] - if filter_domain: - line_ids = self.search(cr, uid, filter_domain, context=context) - where_clause = filter_domain and "AND l.id = ANY(%s)" or "" - cr.execute( - """SELECT partner_id FROM ( - SELECT l.partner_id, p.last_reconciliation_date, SUM(l.debit) AS debit, SUM(l.credit) AS credit, MAX(l.create_date) AS max_date - FROM account_move_line l - RIGHT JOIN account_account a ON (a.id = l.account_id) - RIGHT JOIN res_partner p ON (l.partner_id = p.id) - WHERE a.reconcile IS TRUE - AND l.reconcile_id IS NULL - AND l.state <> 'draft' - %s - GROUP BY l.partner_id, p.last_reconciliation_date - ) AS s - WHERE debit > 0 AND credit > 0 AND (last_reconciliation_date IS NULL OR max_date > last_reconciliation_date) - ORDER BY last_reconciliation_date""" - % where_clause, (line_ids,)) - ids = [x[0] for x in cr.fetchall()] - if not ids: - return [] - - # To apply the ir_rules - partner_obj = self.pool.get('res.partner') - ids = partner_obj.search(cr, uid, [('id', 'in', ids)], context=context) - return partner_obj.name_get(cr, uid, ids, context=context) - - def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False): - move_rec_obj = self.pool.get('account.move.reconcile') - merges = [] - unmerge = [] - total = 0.0 - merges_rec = [] - company_list = [] - if context is None: - context = {} - for line in self.browse(cr, uid, ids, context=context): - if company_list and not line.company_id.id in company_list: - raise UserError(_('To reconcile the entries company should be the same for all entries.')) - company_list.append(line.company_id.id) - - for line in self.browse(cr, uid, ids, context=context): - if line.account_id.currency_id: - currency_id = line.account_id.currency_id - else: - currency_id = line.company_id.currency_id - if line.reconcile_id: - raise UserError(_("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name)) - if line.reconcile_partial_id: - for line2 in line.reconcile_partial_id.line_partial_ids: - if line2.state != 'valid': - raise UserError(_("Journal Item '%s' (id: %s) cannot be used in a reconciliation as it is not balanced!") % (line2.name, line2.id)) - if not line2.reconcile_id: - if line2.id not in merges: - merges.append(line2.id) - if line2.account_id.currency_id: - total += line2.amount_currency - else: - total += (line2.debit or 0.0) - (line2.credit or 0.0) - merges_rec.append(line.reconcile_partial_id.id) - else: - unmerge.append(line.id) - if line.account_id.currency_id: - total += line.amount_currency - else: - total += (line.debit or 0.0) - (line.credit or 0.0) - if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total): - res = self.reconcile(cr, uid, merges+unmerge, context=context, writeoff_acc_id=writeoff_acc_id, writeoff_period_id=writeoff_period_id, writeoff_journal_id=writeoff_journal_id) - return res - # marking the lines as reconciled does not change their validity, so there is no need - # to revalidate their moves completely. - reconcile_context = dict(context, novalidate=True) - r_id = move_rec_obj.create(cr, uid, { - 'type': type, - 'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge) - }, context=reconcile_context) - move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=reconcile_context) - return r_id - - def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None): - account_obj = self.pool.get('account.account') - move_obj = self.pool.get('account.move') - move_rec_obj = self.pool.get('account.move.reconcile') - partner_obj = self.pool.get('res.partner') - currency_obj = self.pool.get('res.currency') - lines = self.browse(cr, uid, ids, context=context) - unrec_lines = filter(lambda x: not x['reconcile_id'], lines) - credit = debit = 0.0 - currency = 0.0 - account_id = False - partner_id = False - if context is None: - context = {} - company_list = [] - for line in self.browse(cr, uid, ids, context=context): - if company_list and not line.company_id.id in company_list: - raise UserError(_('To reconcile the entries company should be the same for all entries.')) - company_list.append(line.company_id.id) - for line in unrec_lines: - if line.state <> 'valid': - raise UserError(_('Entry "%s" is not valid !') % line.name) - credit += line['credit'] - debit += line['debit'] - currency += line['amount_currency'] or 0.0 - account_id = line['account_id']['id'] - partner_id = (line['partner_id'] and line['partner_id']['id']) or False - writeoff = debit - credit - - # Ifdate_p in context => take this date - if context.has_key('date_p') and context['date_p']: - date=context['date_p'] - else: - date = time.strftime('%Y-%m-%d') - - cr.execute('SELECT account_id, reconcile_id '\ - 'FROM account_move_line '\ - 'WHERE id IN %s '\ - 'GROUP BY account_id,reconcile_id', - (tuple(ids), )) - r = cr.fetchall() - #TODO: move this check to a constraint in the account_move_reconcile object - if len(r) != 1: - raise UserError(_('Entries are not of the same account or already reconciled ! ')) - if not unrec_lines: - raise UserError(_('Entry is already reconciled.')) - account = account_obj.browse(cr, uid, account_id, context=context) - if not account.reconcile: - raise UserError(_('The account is not defined to be reconciled !')) - if r[0][1] != None: - raise UserError(_('Some entries are already reconciled.')) - - if (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \ - (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))): - if not writeoff_acc_id: - raise UserError(_('You have to provide an account for the write off/exchange difference entry.')) - if writeoff > 0: - debit = writeoff - credit = 0.0 - self_credit = writeoff - self_debit = 0.0 - else: - debit = 0.0 - credit = -writeoff - self_credit = 0.0 - self_debit = -writeoff - # If comment exist in context, take it - if 'comment' in context and context['comment']: - libelle = context['comment'] - else: - libelle = _('Write-Off') - - cur_obj = self.pool.get('res.currency') - cur_id = False - amount_currency_writeoff = 0.0 - if context.get('company_currency_id',False) != context.get('currency_id',False): - cur_id = context.get('currency_id',False) - for line in unrec_lines: - if line.currency_id and line.currency_id.id == context.get('currency_id',False): - amount_currency_writeoff += line.amount_currency - else: - tmp_amount = cur_obj.compute(cr, uid, line.account_id.company_id.currency_id.id, context.get('currency_id',False), abs(line.debit-line.credit), context={'date': line.date}) - amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount - - writeoff_lines = [ - (0, 0, { - 'name': libelle, - 'debit': self_debit, - 'credit': self_credit, - 'account_id': account_id, - 'date': date, - 'partner_id': partner_id, - 'currency_id': cur_id or (account.currency_id.id or False), - 'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0) - }), - (0, 0, { - 'name': libelle, - 'debit': debit, - 'credit': credit, - 'account_id': writeoff_acc_id, - 'analytic_account_id': context.get('analytic_id', False), - 'date': date, - 'partner_id': partner_id, - 'currency_id': cur_id or (account.currency_id.id or False), - 'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0) - }) - ] - - writeoff_move_id = move_obj.create(cr, uid, { - 'period_id': writeoff_period_id, - 'journal_id': writeoff_journal_id, - 'date':date, - 'state': 'draft', - 'line_id': writeoff_lines - }) - - writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)]) - if account_id == writeoff_acc_id: - writeoff_line_ids = [writeoff_line_ids[1]] - ids += writeoff_line_ids - - # marking the lines as reconciled does not change their validity, so there is no need - # to revalidate their moves completely. - reconcile_context = dict(context, novalidate=True) - r_id = move_rec_obj.create(cr, uid, { - 'type': type, - 'line_id': map(lambda x: (4, x, False), ids), - 'line_partial_ids': map(lambda x: (3, x, False), ids) - }, context=reconcile_context) - # the id of the move.reconcile is written in the move.line (self) by the create method above - # because of the way the line_id are defined: (4, x, False) - for id in ids: - workflow.trg_trigger(uid, 'account.move.line', id, cr) - - if lines and lines[0]: - partner_id = lines[0].partner_id and lines[0].partner_id.id or False - if partner_id and not partner_obj.has_something_to_reconcile(cr, uid, partner_id, context=context): - partner_obj.mark_as_reconciled(cr, uid, [partner_id], context=context) - return r_id - - def view_header_get(self, cr, user, view_id, view_type, context=None): - if context is None: - context = {} - context = self.convert_to_period(cr, user, context=context) - if context.get('account_id', False): - cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], )) - res = cr.fetchone() - if res: - res = _('Entries: ')+ (res[0] or '') - return res - if (not context.get('journal_id', False)) or (not context.get('period_id', False)): - return False - if context.get('search_default_journal_id', False): - context['journal_id'] = context.get('search_default_journal_id') - cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], )) - j = cr.fetchone()[0] or '' - cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], )) - p = cr.fetchone()[0] or '' - if j or p: - return j + (p and (':' + p) or '') - return False - - def onchange_date(self, cr, user, ids, date, context=None): - """ - Returns a dict that contains new values and context - @param cr: A database cursor - @param user: ID of the user currently logged in - @param date: latest value from user input for field date - @param args: other arguments - @param context: context arguments, like lang, time zone - @return: Returns a dict which contains new values, and context - """ - res = {} - if context is None: - context = {} - period_pool = self.pool.get('account.period') - pids = period_pool.find(cr, user, date, context=context) - if pids: - res.update({'period_id':pids[0]}) - context = dict(context, period_id=pids[0]) - return { - 'value':res, - 'context':context, - } - - def _check_moves(self, cr, uid, context=None): - # use the first move ever created for this journal and period - if context is None: - context = {} - cr.execute('SELECT id, state, name FROM account_move WHERE journal_id = %s AND period_id = %s ORDER BY id limit 1', (context['journal_id'],context['period_id'])) - res = cr.fetchone() - if res: - if res[1] != 'draft': - raise UserError(_('The account move (%s) for centralisation has been confirmed.') % res[2]) - return res - - def _remove_move_reconcile(self, cr, uid, move_ids=None, opening_reconciliation=False, context=None): - # Function remove move rencocile ids related with moves - obj_move_line = self.pool.get('account.move.line') - obj_move_rec = self.pool.get('account.move.reconcile') - unlink_ids = [] - if not move_ids: - return True - recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id']) - full_recs = filter(lambda x: x['reconcile_id'], recs) - rec_ids = [rec['reconcile_id'][0] for rec in full_recs] - part_recs = filter(lambda x: x['reconcile_partial_id'], recs) - part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs] - unlink_ids += rec_ids - unlink_ids += part_rec_ids - all_moves = obj_move_line.search(cr, uid, ['|',('reconcile_id', 'in', unlink_ids),('reconcile_partial_id', 'in', unlink_ids)]) - all_moves = list(set(all_moves) - set(move_ids)) - if unlink_ids: - if opening_reconciliation: - raise UserError(_('Opening Entries have already been generated. Please run "Cancel Closing Entries" wizard to cancel those entries and then run this wizard.')) - obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False}) - obj_move_rec.unlink(cr, uid, unlink_ids) - if len(all_moves) >= 2: - obj_move_line.reconcile_partial(cr, uid, all_moves, 'auto',context=context) - return True - - def unlink(self, cr, uid, ids, context=None, check=True): - if context is None: - context = {} - move_obj = self.pool.get('account.move') - self._update_check(cr, uid, ids, context) - result = False - move_ids = set() - for line in self.browse(cr, uid, ids, context=context): - move_ids.add(line.move_id.id) - localcontext = dict(context) - localcontext['journal_id'] = line.journal_id.id - localcontext['period_id'] = line.period_id.id - result = super(account_move_line, self).unlink(cr, uid, [line.id], context=localcontext) - move_ids = list(move_ids) - if check and move_ids: - move_obj.validate(cr, uid, move_ids, context=context) - return result - - def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True): - if context is None: - context={} - move_obj = self.pool.get('account.move') - account_obj = self.pool.get('account.account') - journal_obj = self.pool.get('account.journal') - if isinstance(ids, (int, long)): - ids = [ids] - if vals.get('account_tax_id', False): - raise UserError(_('You cannot change the tax, you should remove and recreate lines.')) - if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']: - raise UserError(_('You cannot use an inactive account.')) - - affects_move = any(f in vals for f in ('account_id', 'journal_id', 'period_id', 'move_id', 'debit', 'credit', 'date')) - - if update_check and affects_move: - self._update_check(cr, uid, ids, context) - - todo_date = None - if vals.get('date', False): - todo_date = vals['date'] - del vals['date'] - - for line in self.browse(cr, uid, ids, context=context): - ctx = context.copy() - if not ctx.get('journal_id'): - if line.move_id: - ctx['journal_id'] = line.move_id.journal_id.id - else: - ctx['journal_id'] = line.journal_id.id - if not ctx.get('period_id'): - if line.move_id: - ctx['period_id'] = line.move_id.period_id.id - else: - ctx['period_id'] = line.period_id.id - #Check for centralisation - journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx) - if journal.centralisation: - self._check_moves(cr, uid, context=ctx) - result = super(account_move_line, self).write(cr, uid, ids, vals, context) - - if affects_move and check and not context.get('novalidate'): - done = [] - for line in self.browse(cr, uid, ids): - if line.move_id.id not in done: - done.append(line.move_id.id) - move_obj.validate(cr, uid, [line.move_id.id], context) - if todo_date: - move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context) - return result - - def _update_journal_check(self, cr, uid, journal_id, period_id, context=None): - journal_obj = self.pool.get('account.journal') - period_obj = self.pool.get('account.period') - jour_period_obj = self.pool.get('account.journal.period') - cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id)) - result = cr.fetchall() - journal = journal_obj.browse(cr, uid, journal_id, context=context) - period = period_obj.browse(cr, uid, period_id, context=context) - for (state,) in result: - if state == 'done': - raise UserError(_('You can not add/modify entries in a closed period %s of journal %s.') % (period.name,journal.name)) - if not result: - jour_period_obj.create(cr, uid, { - 'name': (journal.code or journal.name)+':'+(period.name or ''), - 'journal_id': journal.id, - 'period_id': period.id - }) - return True - - def _update_check(self, cr, uid, ids, context=None): - done = {} - for line in self.browse(cr, uid, ids, context=context): - err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id)) - if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted): - raise UserError(_('You cannot do this modification on a confirmed entry. You can just change some non legal fields or you must unconfirm the journal entry first.\n%s.') % err_msg) - if line.reconcile_id: - raise UserError(_('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg) - t = (line.journal_id.id, line.period_id.id) - if t not in done: - self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context) - done[t] = True - return True - - def create(self, cr, uid, vals, context=None, check=True): - account_obj = self.pool.get('account.account') - tax_obj = self.pool.get('account.tax') - move_obj = self.pool.get('account.move') - cur_obj = self.pool.get('res.currency') - journal_obj = self.pool.get('account.journal') - context = dict(context or {}) - if vals.get('move_id', False): - move = self.pool.get('account.move').browse(cr, uid, vals['move_id'], context=context) - if move.company_id: - vals['company_id'] = move.company_id.id - if move.date and not vals.get('date'): - vals['date'] = move.date - if ('account_id' in vals) and not account_obj.read(cr, uid, [vals['account_id']], ['active'])[0]['active']: - raise UserError(_('You cannot use an inactive account.')) - if 'journal_id' in vals and vals['journal_id']: - context['journal_id'] = vals['journal_id'] - if 'period_id' in vals and vals['period_id']: - context['period_id'] = vals['period_id'] - if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']: - m = move_obj.browse(cr, uid, vals['move_id']) - context['journal_id'] = m.journal_id.id - context['period_id'] = m.period_id.id - #we need to treat the case where a value is given in the context for period_id as a string - if 'period_id' in context and not isinstance(context.get('period_id', ''), (int, long)): - period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id','')) - if len(period_candidate_ids) != 1: - raise UserError(_('No period found or more than one period found for the given date.')) - context['period_id'] = period_candidate_ids[0][0] - if not context.get('journal_id', False) and context.get('search_default_journal_id', False): - context['journal_id'] = context.get('search_default_journal_id') - self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context) - move_id = vals.get('move_id', False) - journal = journal_obj.browse(cr, uid, context['journal_id'], context=context) - vals['journal_id'] = vals.get('journal_id') or context.get('journal_id') - vals['period_id'] = vals.get('period_id') or context.get('period_id') - vals['date'] = vals.get('date') or context.get('date') - if not move_id: - if journal.centralisation: - #Check for centralisation - res = self._check_moves(cr, uid, context) - if res: - vals['move_id'] = res[0] - if not vals.get('move_id', False): - if journal.sequence_id: - #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id) - v = { - 'date': vals.get('date', time.strftime('%Y-%m-%d')), - 'period_id': context['period_id'], - 'journal_id': context['journal_id'] - } - if vals.get('ref', ''): - v.update({'ref': vals['ref']}) - move_id = move_obj.create(cr, uid, v, context) - vals['move_id'] = move_id - else: - raise UserError(_('Cannot create an automatic sequence for this piece.\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.')) - ok = not (journal.type_control_ids or journal.account_control_ids) - if ('account_id' in vals): - account = account_obj.browse(cr, uid, vals['account_id'], context=context) - if journal.type_control_ids: - type = account.user_type - for t in journal.type_control_ids: - if type.code == t.code: - ok = True - break - if journal.account_control_ids and not ok: - for a in journal.account_control_ids: - if a.id == vals['account_id']: - ok = True - break - # Automatically convert in the account's secondary currency if there is one and - # the provided values were not already multi-currency - if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id: - vals['currency_id'] = account.currency_id.id - ctx = {} - if 'date' in vals: - ctx['date'] = vals['date'] - vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id, - account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx) - if not ok: - raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.')) - - result = super(account_move_line, self).create(cr, uid, vals, context=context) - # CREATE Taxes - if vals.get('account_tax_id', False): - tax_id = tax_obj.browse(cr, uid, vals['account_tax_id']) - total = vals['debit'] - vals['credit'] - base_code = 'base_code_id' - tax_code = 'tax_code_id' - account_id = 'account_collected_id' - base_sign = 'base_sign' - tax_sign = 'tax_sign' - if journal.type in ('purchase_refund', 'sale_refund') or (journal.type in ('cash', 'bank') and total < 0): - base_code = 'ref_base_code_id' - tax_code = 'ref_tax_code_id' - account_id = 'account_paid_id' - base_sign = 'ref_base_sign' - tax_sign = 'ref_tax_sign' - base_adjusted = False - for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=False).get('taxes'): - #create the base movement - if base_adjusted == False: - base_adjusted = True - if tax_id.price_include: - total = tax['price_unit'] - newvals = { - 'tax_code_id': tax[base_code], - 'tax_amount': tax[base_sign] * abs(total), - } - if tax_id.price_include: - if tax['price_unit'] < 0: - newvals['credit'] = abs(tax['price_unit']) - else: - newvals['debit'] = tax['price_unit'] - self.write(cr, uid, [result], newvals, context=context) - else: - data = { - 'move_id': vals['move_id'], - 'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''), - 'date': vals['date'], - 'partner_id': vals.get('partner_id', False), - 'ref': vals.get('ref', False), - 'statement_id': vals.get('statement_id', False), - 'account_tax_id': False, - 'tax_code_id': tax[base_code], - 'tax_amount': tax[base_sign] * abs(total), - 'account_id': vals['account_id'], - 'credit': 0.0, - 'debit': 0.0, - } - self.create(cr, uid, data, context) - #create the Tax movement - if not tax['amount'] and not tax[tax_code]: - continue - data = { - 'move_id': vals['move_id'], - 'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''), - 'date': vals['date'], - 'partner_id': vals.get('partner_id',False), - 'ref': vals.get('ref',False), - 'statement_id': vals.get('statement_id', False), - 'account_tax_id': False, - 'tax_code_id': tax[tax_code], - 'tax_amount': tax[tax_sign] * abs(tax['amount']), - 'account_id': tax[account_id] or vals['account_id'], - 'credit': tax['amount']<0 and -tax['amount'] or 0.0, - 'debit': tax['amount']>0 and tax['amount'] or 0.0, - } - self.create(cr, uid, data, context) - del vals['account_tax_id'] - - recompute = journal.env.recompute and context.get('recompute', True) - if check and not context.get('novalidate') and (recompute or journal.entry_posted): - tmp = move_obj.validate(cr, uid, [vals['move_id']], context) - if journal.entry_posted and tmp: - move_obj.button_validate(cr,uid, [vals['move_id']], context) - return result - - def list_periods(self, cr, uid, context=None): - ids = self.pool.get('account.period').search(cr,uid,[]) - return self.pool.get('account.period').name_get(cr, uid, ids, context=context) - - def list_journals(self, cr, uid, context=None): - ng = dict(self.pool.get('account.journal').name_search(cr,uid,'',[])) - ids = ng.keys() - result = [] - for journal in self.pool.get('account.journal').browse(cr, uid, ids, context=context): - result.append((journal.id,ng[journal.id],journal.type, - bool(journal.currency),bool(journal.analytic_journal_id))) - return result diff --git a/addons/account/account_payment.py b/addons/account/account_payment.py new file mode 100644 index 00000000000..ff57e84423d --- /dev/null +++ b/addons/account/account_payment.py @@ -0,0 +1,524 @@ +# -*- coding: utf-8 -*- + +from openerp import models, fields, api, _ +from openerp.exceptions import UserError, ValidationError + +MAP_INVOICE_TYPE_PARTNER_TYPE = { + 'out_invoice': 'customer', + 'out_refund': 'customer', + 'in_invoice': 'supplier', + 'in_refund': 'supplier', +} +# Since invoice amounts are unsigned, this is how we know if money comes in or goes out +MAP_INVOICE_TYPE_PAYMENT_SIGN = { + 'out_invoice': 1, + 'in_refund': 1, + 'in_invoice': -1, + 'out_refund': -1, +} + +class account_payment_method(models.Model): + _name = "account.payment.method" + _description = "Payment Methods" + + name = fields.Char(required=True) + code = fields.Char(required=True) # For internal identification + payment_type = fields.Selection([('inbound', 'Inbound'), ('outbound', 'Outbound')], required=True) + + +class account_abstract_payment(models.AbstractModel): + _name = "account.abstract.payment" + _description = "Contains the logic shared between models which allows to register payments" + + payment_type = fields.Selection([('outbound', 'Send Money'), ('inbound', 'Receive Money')], default='outbound', required=True) + payment_method = fields.Many2one('account.payment.method', string='Payment Method', required=True) + + partner_type = fields.Selection([('customer', 'Customer'), ('supplier', 'Supplier')], default='supplier') + partner_id = fields.Many2one('res.partner', string='Partner') + + amount = fields.Monetary(string='Payment Amount', required=True) + currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id) + payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True, copy=False) + date = fields.Date(string='Accounting Date', copy=False, help="Keep empty to use the payment date.",) + communication = fields.Char() + journal_id = fields.Many2one('account.journal', string='Bank Journal', required=True, domain=[('type', 'in', ('bank', 'cash'))]) + company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True) + + manual_payment_method_selected = fields.Boolean(compute='_compute_manual_payment_method_selected') + hide_payment_method = fields.Boolean(compute='_compute_hide_payment_method', help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'") + + @api.one + @api.constrains('amount') + def _check_amount(self): + if not self.amount > 0.0: + raise ValidationError('The payment amount must be strictly positive.') + + @api.one + @api.constrains('communication') + def _check_communication(self): + """ This method is to be overwritten by payment type modules. The method body would look like : + if self.payment_method == self.env.ref('my_module.payment_method'): + try: + self.communication.decode('ascii') + except UnicodeDecodeError: + raise ValidationError(_t("The communication cannot contain any special character")) + """ + pass + + @api.one + @api.depends('payment_method') + def _compute_manual_payment_method_selected(self): + self.manual_payment_method_selected = self.payment_method.code == 'manual' + + @api.one + @api.depends('payment_type', 'journal_id') + def _compute_hide_payment_method(self): + if not self.journal_id: + self.hide_payment_method = True + return + journal_payment_methods = self.payment_type == 'inbound' and self.journal_id.inbound_payment_methods or self.journal_id.outbound_payment_methods + self.hide_payment_method = len(journal_payment_methods) == 1 and journal_payment_methods[0].code == 'manual' + + @api.onchange('journal_id') + def _onchange_journal(self): + if self.journal_id: + self.currency_id = self.journal_id.currency_id or self.company_id.currency_id + # Set default payment method (we consider the first to be the default one) + payment_methods = self.payment_type == 'inbound' and self.journal_id.inbound_payment_methods or self.journal_id.outbound_payment_methods + self.payment_method = payment_methods and payment_methods[0] or False + # Set payment method domain (restrict to methods enabled for the journal and to selected payment type) + payment_type = self.payment_type in ('outbound', 'transfer') and 'outbound' or 'inbound' + return {'domain': {'payment_method': [('payment_type', '=', payment_type), ('id', 'in', payment_methods.ids)]}} + return {} + + @api.onchange('currency_id') + def _onchange_currency_id(self): + self._set_total_invoices_amount() + + @api.onchange('payment_date') + def _onchange_date(self): + self._set_total_invoices_amount() + + def _get_invoices(self): + """ Return the invoices of the payment. Must be overridden """ + raise NotImplementedError + + def _compute_total_invoices_amount(self): + """ Compute the sum of the residual of invoices, expressed in the payment currency """ + total = 0 + payment_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id + for inv in self._get_invoices(): + total += inv.residual_signed + if self.company_id and self.company_id.currency_id != payment_currency: + total = self.company_id.currency_id.with_context(date=self.payment_date).compute(total, payment_currency) + return abs(total) + + def _set_total_invoices_amount(self): + """ Set self.amount = total residual of invoices, do nothing if the payment does not refer to invoices """ + if len(self._get_invoices()) != 0: + self.amount = self._compute_total_invoices_amount() + + +class account_register_payments(models.TransientModel): + _name = "account.register.payments" + _inherit = 'account.abstract.payment' + _description = "Register payments on multiple invoices" + + @api.onchange('payment_type') + def _onchange_payment_type(self): + if self.payment_type: + return {'domain': {'payment_method': [('payment_type', '=', self.payment_type)]}} + + def _get_invoices(self): + return self.env['account.invoice'].browse(self._context.get('active_ids')) + + @api.model + def default_get(self, fields): + rec = super(account_register_payments, self).default_get(fields) + context = dict(self._context or {}) + active_model = context.get('active_model') + active_ids = context.get('active_ids') + + # Checks on context parameters + if not active_model or not active_ids: + raise UserError(_("Programmation error: wizard action executed without active_model or active_ids in context.")) + if active_model != 'account.invoice': + raise UserError(_("Programmation error: the expected model for this action is 'account.invoice'. The provided one is '%d'." % active_model)) + + # Checks on received invoice records + invoices = self.env[active_model].browse(active_ids) + if any(invoice.state != 'open' for invoice in invoices): + raise UserError(_("You can only register payments for open invoices")) + if any(inv.partner_id != invoices[0].partner_id for inv in invoices): + raise UserError(_("In order to pay multiple invoices at once, they must belong to the same partner.")) + if any(MAP_INVOICE_TYPE_PARTNER_TYPE[inv.type] != MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type] for inv in invoices): + raise UserError(_("You cannot mix customer and supplier invoices in a single payment.")) + if any(inv.currency_id != invoices[0].currency_id for inv in invoices): + raise UserError(_("In order to pay multiple invoices at once, they must use the same currency.")) + + total_amount = sum(inv.residual * MAP_INVOICE_TYPE_PAYMENT_SIGN[inv.type] for inv in invoices) + rec.update({ + 'amount': abs(total_amount), + 'currency_id': invoices[0].currency_id.id, + 'payment_type': total_amount > 0 and 'inbound' or 'outbound', + 'partner_id': invoices[0].partner_id.id, + 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], + }) + return rec + + @api.multi + def create_payment(self): + invoices = self._get_invoices() + payment = self.env['account.payment'].create({ + 'journal_id': self.journal_id.id, + 'payment_method': self.payment_method.id, + 'payment_date': self.payment_date, + 'date': self.date, + 'communication': self.communication, + 'invoice_ids': [(4, inv.id, None) for inv in invoices], + 'payment_type': self.payment_type, + 'amount': self._compute_total_invoices_amount(), + 'currency_id': self.currency_id.id, + 'partner_id': self.partner_id.id, + 'partner_type': self.partner_type, + }) + payment.post() + return {'type': 'ir.actions.act_window_close'} + + +class account_payment(models.Model): + _name = "account.payment" + _inherit = 'account.abstract.payment' + _description = "Payments" + _order = "payment_date desc, name desc" + + @api.one + @api.depends('invoice_ids') + def _get_has_invoices(self): + self.has_invoices = bool(self.invoice_ids) + + @api.one + @api.depends('invoice_ids', 'amount', 'date', 'currency_id') + def _compute_payment_difference(self): + if len(self.invoice_ids) == 0: + return + # A payment on multiple invoices must pay them entirely (and amount be readonly of course) + if len(self.invoice_ids) != 1: + self.payment_difference = 0 + else: + self.payment_difference = self._compute_total_invoices_amount() - self.amount + + name = fields.Char(readonly=True, copy=False) + state = fields.Selection([('draft', 'Draft'), ('posted', 'Posted'), ('sent', 'Sent'), ('reconciled', 'Reconciled')], readonly=True, default='draft', copy=False, string="Status") + + payment_type = fields.Selection(selection_add=[('transfer', 'Internal Transfer')]) + payment_reference = fields.Char(copy=False, readonly=True, help="Reference of the document used to issue this payment. Eg. check number, file name, etc.") + + # Money flows from the journal_id's default_debit_account_id or default_credit_account_id to the destination_account_id + destination_account_id = fields.Many2one('account.account', compute='_compute_destination_account_id', readonly=True) + # For money transfer, money goes from journal_id to a transfer account, then from the transfer account to destination_journal_id + destination_journal_id = fields.Many2one('account.journal', string='Transfer To', domain=[('type', 'in', ('bank', 'cash'))]) + + invoice_ids = fields.Many2many('account.invoice', 'account_invoice_payment_rel', 'payment_id', 'invoice_id', string="Invoices", copy=False, readonly=True) + has_invoices = fields.Boolean(compute="_get_has_invoices", help="Technical field used for usablity purposes") + payment_difference = fields.Float(compute='_compute_payment_difference', readonly=True) + payment_difference_handling = fields.Selection([('open', 'Keep Open'), ('reconcile', 'Reconcile Payment Balance')], default='open', string="Payment Difference", copy=False) + writeoff_account = fields.Many2one('account.account', string="Counterpart Account", domain=[('deprecated', '=', False)], copy=False) + + # FIXME: ondelete='restrict' not working (eg. cancel a bank statement reconciliation with a payment) + move_line_ids = fields.One2many('account.move.line', 'payment_id', readonly=True, copy=False, ondelete='restrict') + + @api.one + @api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id') + def _compute_destination_account_id(self): + if self.invoice_ids: + self.destination_account_id = self.invoice_ids[0].account_id.id + elif self.payment_type == 'transfer': + if not self.company_id.transfer_account_id.id: + raise UserError(_('Transfer account not defined on the company.')) + self.destination_account_id = self.company_id.transfer_account_id.id + elif self.partner_id: + if self.partner_type == 'customer': + self.destination_account_id = self.partner_id.property_account_receivable.id + else: + self.destination_account_id = self.partner_id.property_account_payable.id + + @api.onchange('partner_type') + def _onchange_partner_type(self): + # Set partner_id domain + if self.partner_type: + if getattr(self.partner_id, self.partner_type) is False: + self.partner_id = False + return {'domain': {'partner_id': [(self.partner_type, '=', True)]}} + + @api.onchange('payment_type') + def _onchange_payment_type(self): + # Set default partner type for the payment type + if self.payment_type == 'inbound': + self.partner_type = 'customer' + elif self.payment_type == 'outbound': + self.partner_type = 'supplier' + # Set payment method domain + res = self._onchange_journal() + if not res.get('domain', {}): + res['domain'] = {} + res['domain']['journal_id'] = self.payment_type == 'inbound' and [('at_least_one_inbound', '=', True)] or [('at_least_one_outbound', '=', True)] + res['domain']['journal_id'].append(('type', 'in', ('bank', 'cash'))) + return res + + @api.onchange('invoice_ids') + def _onchange_invoice(self): + if len(self.invoice_ids) == 1: + self.communication = self.invoice_ids[0].reference + self.currency_id = self.invoice_ids[0].currency_id + self.payment_type = self.invoice_ids[0].type in ('out_invoice', 'in_refund') and 'inbound' or 'outbound' + self.partner_type = MAP_INVOICE_TYPE_PARTNER_TYPE[self.invoice_ids[0].type] + self.partner_id = self.invoice_ids[0].partner_id + self._set_total_invoices_amount() + + def _get_invoices(self): + return self.invoice_ids + + @api.multi + def button_journal_entries(self): + return { + 'name': _('Journal Items'), + 'view_type': 'form', + 'view_mode': 'tree', + 'res_model': 'account.move.line', + 'view_id': False, + 'type': 'ir.actions.act_window', + 'domain': [('payment_id', 'in', self.ids)], + } + + @api.multi + def button_invoices(self): + return { + 'name': _('Paid Invoices'), + 'view_type': 'form', + 'view_mode': 'tree', + 'res_model': 'account.invoice', + 'view_id': False, + 'type': 'ir.actions.act_window', + 'domain': [('id', 'in', [x.id for x in self.invoice_ids])], + } + + @api.multi + def button_dummy(self): + return True + + @api.multi + def cancel(self): + for rec in self: + for move in rec.move_line_ids.mapped('move_id'): + if rec.invoice_ids: + move.line_ids.remove_move_reconcile() + move.button_cancel() + move.unlink() + rec.state = 'draft' + + @api.multi + def unlink(self): + if any(rec.state != 'draft' for rec in self): + raise UserError(_("In order to delete a payment, it must first be canceled.")) + return super(account_payment, self).unlink() + + @api.multi + def post(self): + """ Create the journal items for the payment and update the payment's state to 'posted'. + A journal entry is created containing an item in the source liquidity account (selected journal's default_debit or default_credit) + and another in the destination reconciliable account (see _compute_destination_account_id). + If invoice_ids is not empty, there will be one reconciliable move line per invoice to reconcile with. + If the payment is a transfer, a second journal entry is created in the destination journal to receive money from the transfer account. + """ + for rec in self: + + if rec.state != 'draft': + raise UserError(_("Only a draft payment can be posted. Trying to post a payment in state %s.") % rec.state) + + if any(inv.state != 'open' for inv in rec.invoice_ids): + raise ValidationError(_("The payment cannot be processed because an invoice of the payment is not open !")) + + # Use the right sequence to set the name + if rec.payment_type == 'transfer': + sequence = rec.env.ref('account.sequence_payment_transfer') + else: + if rec.partner_type == 'customer': + if rec.payment_type == 'inbound': + sequence = rec.env.ref('account.sequence_payment_customer_invoice') + if rec.payment_type == 'outbound': + sequence = rec.env.ref('account.sequence_payment_customer_refund') + if rec.partner_type == 'supplier': + if rec.payment_type == 'inbound': + sequence = rec.env.ref('account.sequence_payment_supplier_refund') + if rec.payment_type == 'outbound': + sequence = rec.env.ref('account.sequence_payment_supplier_invoice') + rec.name = sequence.with_context(ir_sequence_date=rec.date or rec.payment_date).next_by_id() + + # Create the journal entry + amount = rec.amount * (rec.payment_type in ('outbound', 'transfer') and 1 or -1) + move = rec._create_payment_entry(amount) + + # In case of a transfer, the first journal entry created debited the source liquidity account and credited + # the transfer account. Now we debit the transfer account and credit the destination liquidity account. + if rec.payment_type == 'transfer': + transfer_credit_aml = move.line_ids.filtered(lambda r: r.account_id == rec.company_id.transfer_account_id) + transfer_debit_aml = rec._create_transfer_entry(amount) + (transfer_credit_aml + transfer_debit_aml).reconcile() + + rec.state = 'posted' + + def _create_payment_entry(self, amount): + """ Create a journal entry corresponding to a payment, if the payment references invoice(s) they are reconciled. + Return the journal entry. + """ + aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) + invoice_id = len(self.invoice_ids) == 1 and self.invoice_ids[0] or False + total_debit, total_credit, total_amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) + + move = self.env['account.move'].create(self._get_move_vals()) + + if len(self.invoice_ids) > 1: + total_amount = 0 + for invoice_id in self.invoice_ids: + # Create a reconciliable journal item corresponding to the invoice + inv_amount = -invoice_id.residual * MAP_INVOICE_TYPE_PAYMENT_SIGN[invoice_id.type] + debit = inv_amount > 0 and inv_amount or 0 + credit = inv_amount < 0 and -inv_amount or 0 + amount_currency = self.currency_id != self.company_id.currency_id and self.company_id.currency_id.with_context(date=self.payment_date).compute(inv_amount, self.currency_id) or 0 + # Avoid unbalanced journal entry due to rounding error + total_amount = total_amount + debit - credit + counterpart_aml_dict = self._get_shared_move_line_vals(debit, credit, amount_currency, move.id, invoice_id) + counterpart_aml_dict.update(self._get_counterpart_move_line_vals(invoice_id)) + counterpart_aml_dict.update({'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False}) + counterpart_aml = aml_obj.create(counterpart_aml_dict) + invoice_id.register_payment(counterpart_aml) + total_debit = total_amount > 0 and total_amount or 0 + total_credit = total_amount < 0 and -total_amount or 0 + else: + # Create reconciliable journal item + counterpart_aml_dict = self._get_shared_move_line_vals(total_debit, total_credit, total_amount_currency, move.id, invoice_id) + counterpart_aml_dict.update(self._get_counterpart_move_line_vals(invoice_id)) + counterpart_aml = aml_obj.create(counterpart_aml_dict) + + # Reconcile it with the invoice if present + if invoice_id: + if self.payment_difference_handling == 'reconcile': + invoice_id.register_payment(counterpart_aml, self.writeoff_account, self.journal_id) + else: + invoice_id.register_payment(counterpart_aml) + + liquidity_aml_dict = self._get_shared_move_line_vals(total_credit, total_debit, -total_amount_currency, move.id, invoice_id) + liquidity_aml_dict.update(self._get_liquidity_move_line_vals(-amount)) + aml_obj.create(liquidity_aml_dict) + + move.post() + return move + + def _create_transfer_entry(self, amount): + """ Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconciliable move line + """ + aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) + debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) + amount_currency = self.destination_journal_id.currency_id and self.currency_id.with_context(date=self.payment_date).compute(amount, self.destination_journal_id.currency_id) or 0 + + dst_move = self.env['account.move'].create(self._get_move_vals(self.destination_journal_id)) + + dst_liquidity_aml_dict = self._get_shared_move_line_vals(debit, credit, amount_currency, dst_move.id) + dst_liquidity_aml_dict.update({ + 'name': _('Transfer from %s') % self.journal_id.name, + 'account_id': self.destination_journal_id.default_credit_account_id.id, + 'currency_id': self.destination_journal_id.currency_id.id, + 'payment_id': self.id, + 'journal_id': self.destination_journal_id.id}) + aml_obj.create(dst_liquidity_aml_dict) + + transfer_debit_aml_dict = self._get_shared_move_line_vals(credit, debit, 0, dst_move.id) + transfer_debit_aml_dict.update({ + 'name': self.name, + 'payment_id': self.id, + 'account_id': self.company_id.transfer_account_id.id, + 'journal_id': self.destination_journal_id.id}) + if self.currency_id != self.company_id.currency_id: + transfer_debit_aml_dict.update({ + 'currency_id': self.currency_id.id, + 'amount_currency': -self.amount, + }) + transfer_debit_aml = aml_obj.create(transfer_debit_aml_dict) + dst_move.post() + return transfer_debit_aml + + def _get_move_vals(self, journal=None): + """ Return dict to create the payment move + """ + journal = journal or self.journal_id + if not journal.sequence_id: + raise UserError(_('Configuration Error !'), _('The journal %s does not have a sequence, please specify one.') % journal.name) + if not journal.sequence_id.active: + raise UserError(_('Configuration Error !'), _('The sequence of journal %s is deactivated.') % journal.name) + name = journal.with_context(ir_sequence_date=self.date).sequence_id.next_by_id() + return { + 'name': name, + 'date': self.date or self.payment_date, + 'ref': self.communication or '', + 'company_id': self.company_id.id, + 'journal_id': journal.id, + } + + def _get_shared_move_line_vals(self, debit, credit, amount_currency, move_id, invoice_id=False): + """ Returns values common to both move lines (except for debit, credit and amount_currency which are reversed) + """ + return { + 'partner_id': self.payment_type in ('inbound', 'outbound') and self.partner_id.commercial_partner_id.id or False, + 'invoice': invoice_id and invoice_id.id or False, + 'move_id': move_id, + 'debit': debit, + 'credit': credit, + 'amount_currency': amount_currency or False, + } + + def _get_counterpart_move_line_vals(self, invoice=False): + if self.payment_type == 'transfer': + name = self.name + else: + name = invoice and invoice.number + ': ' or '' + if self.partner_type == 'customer': + if self.payment_type == 'inbound': + name += _("Customer Payment") + elif self.payment_type == 'outbound': + name += _("Customer Refund") + elif self.partner_type == 'supplier': + if self.payment_type == 'inbound': + name += _("Supplier Refund") + elif self.payment_type == 'outbound': + name += _("Supplier Payment") + return { + 'name': name, + 'account_id': self.destination_account_id.id, + 'journal_id': self.journal_id.id, + 'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False, + 'payment_id': self.id, + } + + def _get_liquidity_move_line_vals(self, amount): + name = self.name + if self.payment_type == 'transfer': + name = _('Transfer to %s') % self.destination_journal_id.name + vals = { + 'name': name, + 'account_id': self.payment_type in ('outbound','transfer') and self.journal_id.default_debit_account_id.id or self.journal_id.default_credit_account_id.id, + 'payment_id': self.id, + 'journal_id': self.journal_id.id, + 'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False, + } + + # If the journal has a currency specified, the journal item need to be expressed in this currency + if self.journal_id.currency_id and self.currency_id != self.journal_id.currency_id: + amount = self.currency_id.with_context(date=self.payment_date).compute(amount, self.journal_id.currency_id) + debit, credit, amount_currency = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id) + vals.update({ + 'amount_currency': amount_currency, + 'currency_id': self.journal_id.currency_id.id, + }) + + return vals diff --git a/addons/account/account_pre_install.yml b/addons/account/account_pre_install.yml deleted file mode 100644 index 798c0d954a5..00000000000 --- a/addons/account/account_pre_install.yml +++ /dev/null @@ -1,22 +0,0 @@ -- - I configure automatically if the country is set on the company, mainly for online offers. -- - !python {model: account.installer}: | - modules = self.pool.get('ir.module.module') - wizards = self.pool.get('ir.actions.todo') - wiz = wizards.browse(cr, uid, ref('account.account_configuration_installer_todo')) - part = self.pool.get('res.partner').browse(cr, uid, ref('base.main_partner')) - # if we know the country and the wizard has not yet been executed, we do it - if (part.country_id.id) and (part.country_id.code) and (wiz.state=='open'): - mod = 'l10n_'+part.country_id.code.lower() - ids = modules.search(cr, uid, [ ('name','=',mod) ], context=context) - if ids: - wizards.write(cr, uid, [ref('account.account_configuration_installer_todo')], { - 'state': 'done' - }) - wiz_id = self.create(cr, uid, { - 'charts': mod - }) - self.execute_simple(cr, uid, [wiz_id]) - modules.state_update(cr, uid, ids, - 'to install', ['uninstalled'], context=context) diff --git a/addons/account/account_report.xml b/addons/account/account_report.xml deleted file mode 100644 index d142fd771b1..00000000000 --- a/addons/account/account_report.xml +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml deleted file mode 100644 index a426c2f2bb1..00000000000 --- a/addons/account/account_view.xml +++ /dev/null @@ -1,2645 +0,0 @@ - - - - - - - account.fiscalyear.form - account.fiscalyear - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - account.fiscalyear.tree - account.fiscalyear - - - - - - - - - - - account.fiscalyear.search - account.fiscalyear - - - - - - - - - - - - - - Fiscal Years - account.fiscalyear - form - tree,form - -

- Click to start a new fiscal year. -

- Define your company's financial year according to your needs. A - financial year is a period at the end of which a company's - accounts are made up (usually 12 months). The financial year is - usually referred to by the date in which it ends. For example, - if a company's financial year ends November 30, 2011, then - everything between December 1, 2010 and November 30, 2011 - would be referred to as FY 2011. -

-
-
- - - - - - account.period.form - account.period - -
-
-
- - - - - - - - - - - -
-
-
- - account.period.tree - account.period - - - - - - - - - - - - - - account.period.search - account.period - - - - - - - - - Periods - account.period - form - tree,form - {'search_default_draft': 1} - -

- Click to add a fiscal period. -

- An accounting period typically is a month or a quarter. It - usually corresponds to the periods of the tax declaration. -

-
-
- - - - - account.account.form - account.account - -
-
- - account.account.list - account.account - child_id - - - - - - - - - - - - - - - - - account.account.search - account.account - - - - - - - - - - - - - - - - Accounts - account.account - form - tree,form,graph - - - -

- Click to add an account. -

- An account is part of a ledger allowing your company - to register all kinds of debit and credit transactions. - Companies present their annual accounts in two main parts: the - balance sheet and the income statement (profit and loss - account). The annual accounts of a company are required by law - to disclose a certain amount of information. -

-
-
- - - - - - - account.account.tree - account.account - child_id - - - - - - - - - - - - - - - - Chart of Accounts - account.account - tree - - [('parent_id','=',False)] - - - Unrealized Gain or Loss - account.account - - - - - - - - - - - - - - - - - - Unrealized Gain or Loss - account.account - form - tree - - [('currency_id','!=',False)] - -

- Click to add an account. -

- When doing multi-currency transactions, you may loose or gain - some amount due to changes of exchange rate. This menu gives - you a forecast of the Gain or Loss you'd realized if those - transactions were ended today. Only for accounts having a - secondary currency set. -

-
-
- - - - - - - - - - account.journal.tree - account.journal - - - - - - - - - - - - - account.journal.search - account.journal - - - - - - - - - - - - - - - - - - account.journal.form - account.journal - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - Journals - account.journal - form - tree,form - -

- Click to add a journal. -

- A journal is used to record transactions of all accounting data - related to the day-to-day business. -

- A typical company may use one journal per payment method (cash, - bank accounts, checks), one purchase journal, one sale journal - and one for miscellaneous information. -

-
-
- - - - - - - - - - Reconciliation on Bank Statements - bank_statement_reconciliation_view - {'statement_ids': [active_id]} - - - - account.cash.statement.select - account.bank.statement - - - - - - - - - - - - - - - - - - - account.bank.statement.tree - account.bank.statement - - - - - - - - - - - - - - - - account.bank.statement.search - account.bank.statement - - - - - - - - - - - - - - - - - - - account.bank.statement.form - account.bank.statement - 1 - -
-
- - -
- -
-
-