[MERGE] payment,website_sale: Ease payment setup & flow of ecommerce

Purpose
=======

Ease payment setup & flow of ecommerce

- In Odoo, we have 3 ways to sell:
    - sales -> invoice is needed when you issue quotations
    - eCommerce -> invoice is mostly for B2B but is part of the flow in any case
    - PoS -> invoice is mostly for B2B. It's an option
-> make invoicing an option in ecommerce (still working but invisible)

- Config of payment acquirers is tough to understand: remove order confirmation
    - the invoicing process depends on the company, not on the payment acquirer -> simple invoicing option in ecommerce settings
    - capture manually: only for authorize.net! Somehow it shouldn't be visible for other acquirers

- terms are confusing
    - payment method: payment method is something else in accounting payments.  -> rename to payment acquirers
    - transaction: the difference with payments is not obvious -> rename to payment request

Specification
=============

- New module to add invoicing fields, buttons and menus: account_invoicing -> hide invoicing stuff when starting with eCommerce (not Sales app)
    - new setting in website:
    - keep behavior of current setting
    - if automatic generation of invoice is checked (default value), an invoice must be generated, validated and reconciled when the order is confirmed
    - If "Invoice what is delivered (manual)", users will create the invoice manually from the order.
    - tooltip: Choose the automatic mode if you use to issue an invoice whenever an order is placed. The manual mode allows you to invoice based on what is really delivered. That way you can invoice in several steps, etc. The mode selected here applies to any new product created but not to products already existing. If you want to invoice delivered quantities, you must update the invoicing policy of those products.

- New primary button "Mark as Paid" on SO if status = quotation sent and payment acquirer provider = wire transfer or manual config
    - when you press "Mark as Paid" -> mark payment.transaction as done.

- easy way to cancel payment -> when clicking Cancel Order button: reset the transaction to Pending, break the reconciliation receivable (=invoice) / payment, cancel the payment with an opposite entry. That way everthing comes back to initial state.

- when a payment.transaction is 'authorized' or 'done' -> confirm the SO
- when the transaction is 'done' -> register also a payment in the payment journal set on the payment acquirer (mandatory field, see next item).
    - must work for any payment acquirer, even if provider = wire transfer or manual configuration

- restructure options of payment acquirers:
    - replace Order Confirmation by:
        - Payment Journal
            - existing m2o (journal_id), make mandatory, only show account.journal whose type = bank + context when you create a journal from the field, default value = Bank, display only if account_invoicing is installed
            - tooltip: Payments will be registered into this journal. If you get paid straight on your bank account, select your bank account. If you get paid in batch for several transactions, create a specific payment journal for this payment acquirer to easily manage the bank reconciliation. You hold the amount in a temporary transfer account of your books (created automatically when you create the payment journal). Then when you get paid on your bank account by the payment acquirer, you reconcile the bank statement line with this temporary transfer account. Use reconciliation templates to do it in one-click.
    - Capture Amount Manually   [  ]
        - only visible if acquirer type = Authorize.net
        - tooltip: Capture the amount from Odoo, when the delivery is completed.
        - must be compatible with "Create Invoice Automatically". -> The invoice must be created when the payment transaction is "done"
    - Specific Countries  [  ]
        - if checked show the country field just below
        - tooltip checkbox: Make this payment acquirer available for customers of specific countries.
        - tooltip Countries: If you leave it empty, the payment acquirer will be available for all the countries.
    - Add Extra Fees       [  ]     move up the fees fields

- Relabelling:
    - Payment Methods -> rename to Payment Acquirers (menu items, views, etc.)
        - real payment methods already show up in the Register Payment modal of invoices.
    -   Transactions -> Payment Requests
        - "Transaction" is too close to "Payment". People think it's the same. With payment request, it makes think about a process: payment request -> payment

- Add a "Payment Requests" stat button to SO form to see all the payment attempts + show the status in this list view.
    - if only one payment request, display it in form view
    - + remove payment method + transaction fields below the SO total

Additional remarks:
- mark as paid button only when you can do it
- mark as paid should confirm the order (+ hide Confirm button)
- invoicing settings: faut un peu changer le truc pour séparer la génération automatique de la facture du mode de facturation des produits
- hide radio options when Invoicing option is not checked
- to hide if account_invoicing is not installed:
    - invoicing options in product form
    - SO invoicing status in list view
    - invoicing status in SO form
    - invoice link once order paid + invoice notification in the SO chatter + hide mark as paid button if already done
    - create invoice button in SO
    - orders to invoice in website dashboard
    - in automatic invoicing > mark order as paid > cancel order > reset to quotation -> no way to record the payment again
    show MARK AS PAID in draft quotation status IF payment transaction is set ?
This commit is contained in:
Yannick Tivisse
2017-06-02 16:07:36 +02:00
52 changed files with 733 additions and 489 deletions
+2 -6
View File
@@ -6,11 +6,7 @@
'summary': 'Send Invoices and Track Payments',
'sequence': 30,
'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 vendors and customers.
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.
Core mechanisms for the accounting modules. To display the menuitems, install the module account_invoicing.
""",
'category': 'Accounting',
'website': 'https://www.odoo.com/page/billing',
@@ -75,7 +71,7 @@ You could use this simplified accounting in case you work with an (external) acc
"static/src/xml/account_report_backend.xml",
],
'installable': True,
'application': True,
'application': False,
'auto_install': False,
'post_init_hook': '_auto_install_l10n',
}
+5 -1
View File
@@ -6,9 +6,13 @@
<menuitem name="Invoicing"
id="menu_finance"
groups="group_account_user,group_account_manager,group_account_invoice"
web_icon="account,static/description/icon.png"
web_icon="account_invoicing,static/description/icon.png"
sequence="40"/>
<record id="menu_finance" model="ir.ui.menu">
<field name="active" eval="False"/>
</record>
<menuitem id="menu_finance_receivables" name="Sales" parent="menu_finance" sequence="2"/>
<menuitem id="menu_finance_payables" name="Purchases" parent="menu_finance" sequence="3"/>
<menuitem id="menu_finance_entries" name="Adviser" parent="menu_finance" sequence="4" groups="group_account_manager"/>
+1 -1
View File
@@ -24,7 +24,7 @@
</group>
</group>
<group name="accounting"/>
<group name="invoicing"/>
<group name="invoicing" invisible="1"/>
</page>
</page>
</field>
+1 -1
View File
@@ -14,7 +14,7 @@ It gives the Administrator user access to all accounting features such as journa
It assigns manager and user access rights to the Administrator for the accounting application and only user rights to the Demo user.
""",
'website': 'https://www.odoo.com/page/accounting',
'depends': ['account', 'web_tour'],
'depends': ['account_invoicing', 'web_tour'],
'data': [
'data/account_accountant_data.xml',
'security/account_accountant_security.xml',
@@ -3,7 +3,7 @@
<!-- Rename root menu "Accounting" -->
<!-- Top menu item -->
<menuitem name="Accounting"
web_icon="account,static/description/icon.png"
web_icon="account_accountant,static/description/icon.png"
id="account.menu_finance"
sequence="40"/>
<menuitem id="account.menu_account_config" name="Settings" parent="account.menu_finance_configuration" sequence="0" groups="base.group_system"/>
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+28
View File
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Invoicing Management',
'version': '1.0',
'summary': 'Send Invoices and Track Payments',
'sequence': 30,
'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 vendors and customers.
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': 'Invoicing Management',
'website': 'https://www.odoo.com/page/billing',
'depends': ['account'],
'data': [
'views/account_menuitem_views.xml',
'views/product_template_views.xml',
],
'demo': [
],
'qweb': [
],
'application': True,
}

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account.menu_finance" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
</odoo>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="product_template_form_view_invoice_policy">
<field name="name">product.template.invoice.policy</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="account.product_template_form_view"/>
<field name="arch" type="xml">
<group name="invoicing" position="attributes">
<attribute name="invisible" eval="False"/>
</group>
</field>
</record>
</odoo>
@@ -155,13 +155,12 @@
<record id="payment_acquirer_custom" model="payment.acquirer">
<field name="name">Custom</field>
<field name="auto_confirm">none</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="default_acquirer_button"/>
<field name="module_id" ref="base.module_payment_transfer"/>
<field name="description" type="html">
<p>
A generic payment method to send payment instructions to any payment acquirer.
A generic payment acquirer to send payment instructions to any payment acquirer.
</p>
<ul class="list-inline">
<li><i class="fa fa-check"/>Online Payment</li>
+4 -4
View File
@@ -10,7 +10,7 @@ class AccountPayment(models.Model):
_inherit = 'account.payment'
payment_transaction_id = fields.Many2one('payment.transaction', string="Payment Transaction")
payment_token_id = fields.Many2one('payment.token', string="Saved payment token", domain=[('acquirer_id.auto_confirm', '!=', 'authorize')],
payment_token_id = fields.Many2one('payment.token', string="Saved payment token", domain=[('acquirer_id.capture_manually', '=', False)],
help="Note that tokens from acquirers set to only authorize transactions (instead of capturing the amount) are not available.")
@api.onchange('partner_id')
@@ -18,14 +18,14 @@ class AccountPayment(models.Model):
res = {}
if self.partner_id:
partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids
res['domain'] = {'payment_token_id': [('partner_id', 'in', partners.ids), ('acquirer_id.auto_confirm', '!=', 'authorize')]}
res['domain'] = {'payment_token_id': [('partner_id', 'in', partners.ids), ('acquirer_id.capture_manually', '=', False)]}
return res
@api.onchange('payment_method_id', 'journal_id')
def _onchange_payment_method(self):
if self.payment_method_code == 'electronic':
self.payment_token_id = self.env['payment.token'].search([('partner_id', '=', self.partner_id.id), ('acquirer_id.auto_confirm', '!=', 'authorize')], limit=1)
self.payment_token_id = self.env['payment.token'].search([('partner_id', '=', self.partner_id.id), ('acquirer_id.capture_manually', '=', False)], limit=1)
else:
self.payment_token_id = False
@@ -38,7 +38,7 @@ class AccountPayment(models.Model):
return account_payment
def _do_payment(self):
if self.payment_token_id.acquirer_id.auto_confirm == 'authorize':
if self.payment_token_id.acquirer_id.capture_manually:
raise ValidationError(_('This feature is not available for payment acquirers set to the "Authorize" mode.\n'
'Please use a token from another provider than %s.') % self.payment_token_id.acquirer_id.name)
reference = "P-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
+26 -17
View File
@@ -71,15 +71,22 @@ class PaymentAcquirer(models.Model):
website_published = fields.Boolean(
'Visible in Portal / Website', copy=False,
help="Make this payment acquirer available (Customer invoices, etc.)")
auto_confirm = fields.Selection([
('none', 'No automatic confirmation'),
('authorize', 'Authorize the amount and confirm the order on acquirer confirmation (capture manually)'),
('confirm_so', 'Authorize & capture the amount and confirm the order on acquirer confirmation'),
('generate_and_pay_invoice', 'Authorize & capture the amount, confirm the order and auto-validate the invoice on acquirer confirmation')],
string='Order Confirmation', default='confirm_so', required=True)
# Formerly associated to `authorize` option from auto_confirm
capture_manually = fields.Boolean(string="Capture Amount Manually",
help="Capture the amount from Odoo, when the delivery is completed.")
# Formerly associated to `generate_and_pay_invoice` option from auto_confirm
journal_id = fields.Many2one(
'account.journal', 'Payment Journal',
help="Account journal used for automatic payment reconciliation.")
'account.journal', 'Payment Journal', domain=[('type', '=', 'bank')],
default=lambda self: self.env['account.journal'].search([('type', '=', 'bank')], limit=1),
help="""Payments will be registered into this journal. If you get paid straight on your bank account,
select your bank account. If you get paid in batch for several transactions, create a specific
payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
the amount in a temporary transfer account of your books (created automatically when you create
the payment journal). Then when you get paid on your bank account by the payment acquirer, you
reconcile the bank statement line with this temporary transfer account. Use reconciliation
templates to do it in one-click.""")
specific_countries = fields.Boolean(string="Specific Countries",
help="If you leave it empty, the payment acquirer will be available for all the countries.")
country_ids = fields.Many2many(
'res.country', 'payment_country_rel',
'payment_id', 'country_id', 'Countries',
@@ -116,7 +123,7 @@ class PaymentAcquirer(models.Model):
"If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you "
"issue an invoice.")
token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support')
authorize_implemented = fields.Boolean('Authorize Mechanism Supported', compute='_compute_feature_support')
fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_feature_support')
fees_active = fields.Boolean('Add Extra Fees')
fees_dom_fixed = fields.Float('Fixed domestic fees')
@@ -142,11 +149,11 @@ class PaymentAcquirer(models.Model):
"resized as a 64x64px image, with aspect ratio preserved. "
"Use this field anywhere a small image is required.")
@api.multi
def _compute_feature_support(self):
feature_support = self._get_feature_support()
for acquirer in self:
acquirer.fees_implemented = acquirer.provider in feature_support['fees']
acquirer.authorize_implemented = acquirer.provider in feature_support['authorize']
acquirer.token_implemented = acquirer.provider in feature_support['tokenize']
@api.multi
@@ -158,13 +165,6 @@ class PaymentAcquirer(models.Model):
return False
return True
@api.constrains('auto_confirm')
def _check_authorization_support(self):
for acquirer in self:
if acquirer.auto_confirm == 'authorize' and acquirer.provider not in self._get_feature_support()['authorize']:
raise ValidationError(_('You cannot capture payments manually with this payment method. Please choose another Order Confirmation mode (in Configuration tab).'))
return True
_constraints = [
(_check_required_if_provider, 'Required fields not filled', []),
]
@@ -185,13 +185,22 @@ class PaymentAcquirer(models.Model):
@api.model
def create(self, vals):
image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).create(vals)
@api.multi
def write(self, vals):
image_resize_images(vals)
vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).write(vals)
def _check_journal_id(self, vals):
if not vals.get('journal_id', False):
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if default_journal:
vals.update({'journal_id': default_journal.id})
return vals
@api.multi
def toggle_website_published(self):
self.write({'website_published': not self.website_published})
+17 -30
View File
@@ -8,16 +8,6 @@
id='root_payment_menu'
parent='account.menu_finance_configuration'/>
<!-- Payment acquirers -->
<record id="payment_transaction_action_child" model="ir.actions.act_window">
<field name="name">Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('acquirer_id','=',active_id)]</field>
<field name="context">{'default_acquirer_id': active_id}</field>
</record>
<record id="acquirer_form" model="ir.ui.view">
<field name="name">payment.acquirer.form</field>
<field name="model">payment.acquirer</field>
@@ -26,16 +16,11 @@
<field name="environment" invisible='1'/>
<field name="fees_implemented" invisible='1'/>
<field name="token_implemented" invisible='1'/>
<field name="authorize_implemented" invisible="1"/>
<sheet>
<field name="module_id" invisible="1"/>
<field name="module_state" invisible="1"/>
<div class="oe_button_box" name="button_box" attrs="{'invisible': ['&amp;', ('module_id', '!=', False), ('module_state', '!=', 'installed')]}">
<button name="toggle_website_published"
class="oe_stat_button"
type="object" icon="fa-globe">
<field name="website_published" widget="boolean_button"
options='{"terminology": {"string_true": "Published", "hover_false": "Publish"}}'/>
</button>
<!-- transfer referenced here due to view inheritance issue in current master (post-saas-16) -->
<button name="toggle_environment_value"
attrs="{'invisible': ['|', ('environment', '=', 'test'), ('provider', '=', 'transfer')]}"
@@ -56,8 +41,11 @@
<span class="o_stat_text">Environment</span>
</div>
</button>
<button class="oe_stat_button" name="%(payment_transaction_action_child)d"
type="action" icon="fa-money" string="Payments">
<button name="toggle_website_published"
class="oe_stat_button"
type="object" icon="fa-globe">
<field name="website_published" widget="boolean_button"
options='{"terminology": {"string_true": "Published", "hover_false": "Publish"}}'/>
</button>
</div>
<field name="image" widget='image' class="oe_avatar" options='{"preview_image": "image_medium"}'/>
@@ -93,10 +81,16 @@
</page>
<page string="Configuration">
<group name="acquirer_config">
<field name="auto_confirm" widget="radio"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': [('token_implemented', '=', False)]}"/>
<field name="journal_id" attrs="{'required': [('auto_confirm', '=', 'generate_and_pay_invoice')], 'invisible': [('auto_confirm', '!=', 'generate_and_pay_invoice')]}"/>
<field name="country_ids" widget="many2many_tags"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
<field name="fees_dom_fixed" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_dom_var" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_int_fixed" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_int_var" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="journal_id" context="{'default_type': 'bank'}"/>
<field name="specific_countries"/>
<field name="country_ids" widget="many2many_tags" attrs="{'invisible': [('specific_countries', '=', False)]}"/>
<label for="view_template_id" groups="base.group_no_one"/>
<div groups="base.group_no_one">
<field name="view_template_id" nolabel="1"/>
@@ -119,13 +113,6 @@
</div>
<field name="registration_view_template_id" groups="base.group_no_one"/>
</group>
<group name="fees_config" attrs="{'invisible': [('fees_implemented', '=', False)]}">
<field name="fees_active"/>
<field name="fees_dom_fixed" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_dom_var" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_int_fixed" attrs="{'invisible': [('fees_active', '=', False)]}"/>
<field name="fees_int_var" attrs="{'invisible': [('fees_active', '=', False)]}"/>
</group>
</page>
</notebook>
</sheet>
@@ -203,13 +190,13 @@
</record>
<record id="action_payment_acquirer" model="ir.actions.act_window">
<field name="name">Payment Methods</field>
<field name="name">Payment Acquirers</field>
<field name="res_model">payment.acquirer</field>
<field name='view_type'>form</field>
<field name='view_mode'>kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a payment method.
Click to create a payment acquirer.
</p>
</field>
</record>
+2 -2
View File
@@ -66,7 +66,7 @@ class PaymentAcquirerAuthorize(models.Model):
'x_trans_key': self.authorize_transaction_key,
'x_amount': str(values['amount']),
'x_show_form': 'PAYMENT_FORM',
'x_type': 'AUTH_CAPTURE' if self.auto_confirm != 'authorize' else 'AUTH_ONLY',
'x_type': 'AUTH_CAPTURE' if not self.capture_manually else 'AUTH_ONLY',
'x_method': 'CC',
'x_fp_sequence': '%s%s' % (self.id, int(time.time())),
'x_version': '3.1',
@@ -246,7 +246,7 @@ class TxAuthorize(models.Model):
def authorize_s2s_do_transaction(self, **data):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
if self.acquirer_id.auto_confirm != "authorize":
if not self.acquirer_id.capture_manually:
res = transaction.auth_and_capture(self.payment_token_id, self.amount, self.reference)
else:
res = transaction.authorize(self.payment_token_id, self.amount, self.reference)
@@ -25,7 +25,7 @@ class AuthorizeCommon(PaymentAcquirerCommon):
# get the authorize account
self.authorize = self.env.ref('payment.payment_acquirer_authorize')
# Be sure to be in 'capture' mode
self.authorize.auto_confirm = 'confirm_so'
# self.authorize.auto_confirm = 'confirm_so'
@odoo.tests.common.at_install(True)
@@ -219,7 +219,7 @@ class AuthorizeForm(AuthorizeCommon):
# switch to 'authorize only'
# create authorize only s2s transaction & capture it
self.authorize.auto_confirm = 'authorize'
self.authorize.capture_manually = True
transaction = self.env['payment.transaction'].create({
'amount': 500,
'acquirer_id': authorize.id,
@@ -236,7 +236,7 @@ class AuthorizeForm(AuthorizeCommon):
self.assertEqual(transaction.state, 'done')
# create authorize only s2s transaction & void it
self.authorize.auto_confirm = 'authorize'
self.authorize.capture_manually = True
transaction = self.env['payment.transaction'].create({
'amount': 500,
'acquirer_id': authorize.id,
@@ -8,11 +8,10 @@
<field name="provider">transfer</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="transfer_acquirer_button"/>
<field name="auto_confirm">none</field>
<field name="environment">test</field>
<field name="website_published" eval="True"/>
<field name="pre_msg"><![CDATA[
<p>Transfer information will be provided after choosing the payment method.</p>]]>
<p>Transfer information will be provided after choosing the payment aquirer.</p>]]>
</field>
</record>
@@ -17,9 +17,6 @@
<field name="error_msg" position="attributes">
<attribute name="attrs">{'invisible': [('provider', '=', 'transfer')]}</attribute>
</field>
<field name="auto_confirm" position="attributes">
<attribute name="attrs">{'invisible': [('provider', '=', 'transfer')]}</attribute>
</field>
</field>
</record>
</odoo>
+1 -1
View File
@@ -94,6 +94,6 @@
<field name="help">This report performs analysis on your quotations and sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
</record>
<menuitem name="Sales" action="action_order_report_all" id="menu_report_product_all" parent="sales_team.menu_sale_report" sequence="8"/>
<menuitem name="Sales" action="action_order_report_all" id="menu_report_product_all" parent="sales_team.menu_sale_report" sequence="8" active="False"/>
</odoo>
@@ -57,5 +57,6 @@
sequence="2"
name="Report Layout Categories"
groups="sale.group_sale_layout"
active="False"
/>
</odoo>
+23 -26
View File
@@ -8,31 +8,29 @@
-->
<menuitem name="Sales"
id="sales_team.menu_base_partner"
web_icon="sale,static/description/icon.png"/>
<record id="sales_team.menu_base_partner" model="ir.ui.menu">
<field name="active" eval="False"/>
</record>
web_icon="sale,static/description/icon.png"
active="False"/>
<menuitem
id= "menu_sales_config"
parent= "sales_team.menu_sale_config"
sequence= "3"
name= "Sales"
groups= "sales_team.group_sale_manager,sales_team.group_sale_salesman,account.group_account_manager,account.group_account_user"/>
groups= "sales_team.group_sale_manager,sales_team.group_sale_salesman,account.group_account_manager,account.group_account_user"
active="False"/>
<menuitem id="product_menu_catalog" name="Catalog" action="product.product_template_action" parent="sales_team.menu_base_partner" sequence="7"/>
<menuitem id="menu_product" name="Product Variants" parent="product_menu_catalog" sequence="2" groups="product.group_product_variant"/>
<menuitem id="product_menu_catalog" name="Catalog" action="product.product_template_action" parent="sales_team.menu_base_partner" sequence="7" active="False"/>
<menuitem id="menu_product" name="Product Variants" parent="product_menu_catalog" sequence="2" groups="product.group_product_variant" active="False"/>
<menuitem id="sales_team.menu_sales" name="Sales" parent="sales_team.menu_base_partner" sequence="4"/>
<menuitem action="product.product_template_action" id="menu_product_template_action" parent="product_menu_catalog" sequence="1" />
<menuitem id="sale.prod_config_main" name="Products" parent="sales_team.menu_sale_config" sequence="5"/>
<menuitem action="product.attribute_action" id="menu_attribute_action" parent="sale.prod_config_main" sequence="1" groups="product.group_product_variant"/>
<menuitem action="product.variants_action" id="menu_variants_action" parent="sale.prod_config_main" sequence="2" groups="product.group_product_variant"/>
<menuitem id="menu_products" action="product.product_normal_action_sell" parent="product_menu_catalog" groups="product.group_product_variant" sequence="2"/>
<menuitem id="next_id_16" name="Units of Measure" parent="sale.prod_config_main" sequence="6" groups="product.group_uom"/>
<menuitem action="product.product_uom_form_action" id="menu_product_uom_form_action" parent="sale.prod_config_main" sequence="7" groups="product.group_uom"/>
<menuitem action="product.product_uom_categ_form_action" id="menu_product_uom_categ_form_action" parent="sale.prod_config_main" sequence="8" groups="product.group_uom"/>
<menuitem id="menu_product_pricelist_main" name="Pricelists" parent="product_menu_catalog" action="product.product_pricelist_action2" groups="product.group_sale_pricelist" sequence="3"/>
<menuitem action="product.product_template_action" id="menu_product_template_action" parent="product_menu_catalog" sequence="1" active="False"/>
<menuitem id="sale.prod_config_main" name="Products" parent="sales_team.menu_sale_config" sequence="5" active="False"/>
<menuitem action="product.attribute_action" id="menu_attribute_action" parent="sale.prod_config_main" sequence="1" groups="product.group_product_variant" active="False"/>
<menuitem action="product.variants_action" id="menu_variants_action" parent="sale.prod_config_main" sequence="2" groups="product.group_product_variant" active="False"/>
<menuitem id="menu_products" action="product.product_normal_action_sell" parent="product_menu_catalog" groups="product.group_product_variant" sequence="2" active="False"/>
<menuitem id="next_id_16" name="Units of Measure" parent="sale.prod_config_main" sequence="6" groups="product.group_uom" active="False"/>
<menuitem action="product.product_uom_form_action" id="menu_product_uom_form_action" parent="sale.prod_config_main" sequence="7" groups="product.group_uom" active="False"/>
<menuitem action="product.product_uom_categ_form_action" id="menu_product_uom_categ_form_action" parent="sale.prod_config_main" sequence="8" groups="product.group_uom" active="False"/>
<menuitem id="menu_product_pricelist_main" name="Pricelists" parent="product_menu_catalog" action="product.product_pricelist_action2" groups="product.group_sale_pricelist" sequence="3" active="False"/>
<record id="view_sale_order_calendar" model="ir.ui.view">
<field name="name">sale.order.calendar</field>
@@ -119,7 +117,7 @@
<field name="user_id"/>
<field name="amount_total" sum="Total Tax Included" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="invoice_status"/>
<field name="invoice_status" invisible="1"/>
<field name="state" invisible="1"/>
</tree>
</field>
@@ -159,7 +157,7 @@
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary"/>
<button name="action_quotation_send" type="object" string="Send PRO-FORMA" groups="sale.group_proforma_sales" class="btn-primary" attrs="{'invisible': ['|', ('state', 'in', ['sent', 'sale']), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
<button name="print_quotation" string="Print" type="object" states="draft" class="btn-primary o_sale_print"/>
<button name="action_confirm" states="sent" string="Confirm Sale" class="btn-primary" type="object" />
<button name="action_confirm" id="action_confirm" states="sent" string="Confirm Sale" class="btn-primary" type="object" />
<button name="action_confirm" states="draft" string="Confirm Sale" type="object" />
<button name="action_quotation_send" type="object" string="Send PRO-FORMA" groups="sale.group_proforma_sales" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('invoice_count','&gt;=',1)]}" context="{'proforma': True}"/>
<button name="print_quotation" string="Print" type="object" states="sent,sale" class="o_sale_print"/>
@@ -351,8 +349,7 @@
<group name="sale_pay" string="Invoicing">
<field name="date_order" attrs="{'invisible': [('state', 'in', ['sale', 'done', 'cancel'])]}"/>
<field name="fiscal_position_id" options="{'no_create': True}"/>
<field name="invoice_status"
attrs="{'invisible': [('state', 'not in', ('sale','done'))]}"/>
<field name="invoice_status" states="sale,done" invisible="1"/>
</group>
<group string="Reporting" name="technical" groups="base.group_no_one">
<field groups="base.group_no_one" name="origin"/>
@@ -473,7 +470,7 @@
<menuitem action="action_orders"
id="menu_sale_order" parent="sales_team.menu_sales"
sequence="12" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager"/>
sequence="12" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager" active="False"/>
<record id="action_orders_to_invoice" model="ir.actions.act_window">
<field name="name">Orders to Invoice</field>
@@ -495,10 +492,10 @@
</record>
<menuitem name="Invoicing"
id="menu_sale_invoicing" parent="sales_team.menu_base_partner"
sequence="5"/>
sequence="5" active="False"/>
<menuitem action="action_orders_to_invoice"
id="menu_sale_order_invoice" parent="sale.menu_sale_invoicing"
sequence="2"/>
sequence="2" active="False"/>
<record id="action_orders_upselling" model="ir.actions.act_window">
<field name="name">Orders to Upsell</field>
@@ -523,7 +520,7 @@
</record>
<menuitem action="action_orders_upselling"
id="menu_sale_order_upselling" parent="sale.menu_sale_invoicing"
sequence="5"/>
sequence="5" active="False"/>
<record id="action_quotations" model="ir.actions.act_window">
@@ -550,7 +547,7 @@
<menuitem id="menu_sale_quotations"
action="action_quotations" parent="sales_team.menu_sales"
sequence="11"/>
sequence="11" active="False"/>
<record id="view_order_line_tree" model="ir.ui.view">
<field name="name">sale.order.line.tree</field>
+1 -1
View File
@@ -41,7 +41,7 @@ The Dashboard for the Sales Manager will include
* Monthly Turnover (Graph)
""",
'website': 'https://www.odoo.com/page/sales',
'depends': ['sale'],
'depends': ['sale', 'account_invoicing'],
'data': [
'views/sale_management_views.xml',
'views/sale_management_templates.xml',
@@ -3,4 +3,86 @@
<record id="sales_team.menu_base_partner" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_report_product_all" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.Report_configuration" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_sales_config" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.product_menu_catalog" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_product" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_product_template_action" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.prod_config_main" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_attribute_action" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_variants_action" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_products" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.next_id_16" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_product_uom_form_action" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_product_uom_categ_form_action" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_product_pricelist_main" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_sale_order" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_sale_invoicing" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_sale_order_invoice" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_sale_order_upselling" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale.menu_sale_quotations" model="ir.ui.menu">
<field name="active" eval="True"/>
</record>
<record id="sale_order_view_tree" model="ir.ui.view">
<field name="name">sale.order.tree</field>
<field name="model">sale.order</field>
<field name="priority">2</field>
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<field name="invoice_status" position="attributes">
<attribute name="invisible" eval="False"/>
</field>
</field>
</record>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='sale_pay']/field[@name='invoice_status']" position="attributes">
<attribute name="invisible" eval="False"/>
</xpath>
</field>
</record>
</odoo>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="header_footer_custom_payment" inherit_id="website.footer_default" customize_show="True" active="False" name="Payment Methods">
<template id="header_footer_custom_payment" inherit_id="website.footer_default" customize_show="True" active="False" name="Payment Acquirers">
<xpath expr="//div[@id='info']" position="inside">
<h4 class="payment_method_title">Payment Method</h4>
<h4 class="payment_method_title">Payment Acquirer</h4>
<ul class="payment_method_list list-inline">
<t t-foreach="request.website.payment_acquirers()" t-as="acquirer">
<li>
@@ -24,21 +24,21 @@
</xpath>
</template>
<template id="pay_methods" name="Payment Methods">
<template id="pay_methods" name="Payment Acquirers">
<t t-call="website.layout">
<t t-set="additional_title">Payment Methods</t>
<t t-set="additional_title">Payment Acquirers</t>
<div class="wrap">
<div class="container">
<div class="row">
<div class="col-sm-6">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li>Payment Methods</li>
<li>Payment Acquirers</li>
</ol>
</div>
</div>
<h1>Payment Methods</h1>
<h4 t-if="acquirers">Add a new payment method:</h4>
<h1>Payment Acquirers</h1>
<h4 t-if="acquirers">Add a new payment acquirer:</h4>
<div t-if="acquirers" class="row">
<div class="col-md-6">
<div t-if="error_message" class="alert alert-danger">
@@ -58,7 +58,7 @@
</div>
</div>
<div class="clearfix"></div>
<h4>Delete a payment method:</h4>
<h4>Delete a payment acquirer:</h4>
<div class="row">
<div class="col-md-6">
<form method="post" action="/website_payment/delete/">
@@ -83,7 +83,7 @@
<template id="pay_meth_link" inherit_id="website_portal.portal_layout" customize_show="True">
<xpath expr="//div[@class='o_my_details']" position="inside">
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False)])">
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment methods</a></div>
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment acquirers</a></div>
</t>
</xpath>
</template>
+10 -9
View File
@@ -11,6 +11,7 @@ class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
# link with the sales order
# YTI FIXME: The auto_join seems useless
sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True)
def _generate_and_pay_invoice(self, tx, acquirer_name):
@@ -29,13 +30,15 @@ class PaymentTransaction(models.Model):
acquirer_name, created_invoice.name, created_invoice.id, tx.sale_order_id.name, tx.sale_order_id.id)
created_invoice.action_invoice_open()
if tx.acquirer_id.journal_id:
if not tx.acquirer_id.journal_id:
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if not default_journal:
_logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)',
acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.acquirer_id.journal_id = default_journal
created_invoice.pay_and_reconcile(tx.acquirer_id.journal_id, pay_amount=created_invoice.amount_total)
if created_invoice.payment_ids:
created_invoice.payment_ids[0].payment_transaction_id = tx
else:
_logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)',
acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
else:
_logger.warning('<%s> transaction completed, could not auto-generate invoice for %s (ID %s)',
acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
@@ -69,15 +72,13 @@ class PaymentTransaction(models.Model):
if amount_matches:
if not acquirer_name:
acquirer_name = tx.sale_order_id.payment_acquirer_id.provider or 'unknown'
if tx.state == 'authorized' and tx.acquirer_id.auto_confirm == 'authorize':
if tx.state == 'authorized' and tx.acquirer_id.capture_manually:
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.with_context(send_email=True).action_confirm()
if tx.state == 'done' and tx.acquirer_id.auto_confirm in ['confirm_so', 'generate_and_pay_invoice']:
if tx.state == 'done':
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.with_context(send_email=True).action_confirm()
if tx.acquirer_id.auto_confirm == 'generate_and_pay_invoice':
self._generate_and_pay_invoice(tx, acquirer_name)
self._generate_and_pay_invoice(tx, acquirer_name)
elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft':
_logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.force_quotation_send()
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, exceptions, fields, models
from odoo import api, exceptions, fields, models, _
from odoo.exceptions import ValidationError
class SaleOrder(models.Model):
@@ -9,11 +11,18 @@ class SaleOrder(models.Model):
_inherit = 'sale.order'
website_url = fields.Char('Website URL', compute='_website_url', help='The full URL to access the document through the website.')
payment_transaction_count = fields.Integer(compute='_compute_payment_transaction_count', string="Number of payment transactions")
def _website_url(self):
for so in self:
so.website_url = '/my/orders/%s' % (so.id)
def _compute_payment_transaction_count(self):
transaction_data = self.env['payment.transaction'].read_group([('sale_order_id', 'in', self.ids)], ['sale_order_id'], ['sale_order_id'])
mapped_data = dict([(m['sale_order_id'][0], m['sale_order_id_count']) for m in transaction_data])
for order in self:
order.payment_transaction_count = mapped_data.get(order.id, 0)
@api.multi
def get_access_action(self):
""" Instead of the classic form view, redirect to the online quote for
@@ -62,6 +71,24 @@ class SaleOrder(models.Model):
model=self._name,
res_id=self.id)[self.partner_id.id]
def action_view_transaction(self):
action = {
'type': 'ir.actions.act_window',
'name': 'Payment Transactions',
'res_model': 'payment.transaction',
}
if self.payment_transaction_count == 1:
action.update({
'res_id': self.env['payment.transaction'].search([('sale_order_id', '=', self.id)]).id,
'view_mode': 'form',
})
else:
action.update({
'view_mode': 'tree,form',
'domain': [('sale_order_id', '=', self.id)],
})
return action
class SaleOrderLine(models.Model):
@@ -1,15 +1,26 @@
<?xml version="1.0"?>
<odoo>
<record id="view_account_invoice_filter_share" model="ir.ui.view">
<field name="name">account.invoice.select.share</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="user_id"]' position="attributes">
<attribute name="domain">[('share','=', False)]</attribute>
</xpath>
</field>
</record>
<record id="view_account_invoice_filter_share" model="ir.ui.view">
<field name="name">account.invoice.select.share</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="user_id"]' position="attributes">
<attribute name="domain">[('share','=', False)]</attribute>
</xpath>
</field>
</record>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_transaction" type="object" class="oe_stat_button" icon="fa-money" attrs="{'invisible': [('payment_transaction_count', '=', 0)]}">
<field name="payment_transaction_count" widget="statinfo" string="Transactions"/>
</button>
</xpath>
</field>
</record>
</odoo>
@@ -15,7 +15,7 @@
<button name="%(website_quote.action_sale_quotation_template)d" icon="fa-arrow-right" type="action" string="Quotation Templates" class="btn-link"/>
</div>
<div attrs="{'invisible': [('module_website_quote','=',False)]}">
<button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Methods" class="btn-link"/>
<button name='%(payment.action_payment_acquirer)d' icon="fa-arrow-right" type="action" string="Payment Acquirers" class="btn-link"/>
</div>
</xpath>
</field>
@@ -120,5 +120,5 @@
<menuitem id="menu_sale_quote_template" action="action_sale_quotation_template" parent="sale.menu_sales_config" sequence="1" name="Quotation Templates" groups="sales_team.group_sale_salesman,sales_team.group_sale_manager"/>
<menuitem id='payment_acquirer_menu_sale' action='payment.action_payment_acquirer' parent='sale.menu_sales_config' sequence='3' name='Payment Methods' groups="sales_team.group_sale_salesman"/>
<menuitem id='payment_acquirer_menu_sale' action='payment.action_payment_acquirer' parent='sale.menu_sales_config' sequence='3' name='Payment Acquirers' groups="sales_team.group_sale_salesman"/>
</odoo>
@@ -342,7 +342,7 @@
<!-- begin website_sale code -->
<div t-if="acquirers" id="payment_method" class="row text-left">
<div class="col-md-6">
<h4>Payment Method:</h4>
<h4>Payment Acquirer:</h4>
<ul class="list-unstyled">
<t t-set="payment_methods_available" t-value="False"/>
<li t-foreach="acquirers or []" t-as="acquirer">
+1 -1
View File
@@ -768,7 +768,7 @@ class WebsiteSale(http.Controller):
tx.partner_id == tx.sale_order_id.partner_id):
try:
s2s_result = tx.s2s_do_transaction()
valid_state = 'authorized' if tx.acquirer_id.auto_confirm == 'authorize' else 'done'
valid_state = 'authorized' if tx.acquirer_id.capture_manually else 'done'
if not s2s_result or tx.state != valid_state:
return dict(success=False, error=_("Payment transaction failed (%s)") % tx.state_message)
else:
@@ -115,14 +115,14 @@
<div class="col-sm-1 col-md-2 icon"><span class="fa fa-question-circle "/></div>
<div class="col-sm-11 col-md-10">
<p><strong>Reassurance arguments</strong><br/>
Anticipate your customers questions &amp; worries on practical details like shipping rates &amp; policies, return &amp; replacement policies, payment methods &amp; security and your <a href="https://www.odoo.com/documentation/user/online/ecommerce/managing_products/stock.html" target="_blank">product's availability (In Stock, Not Available, etc.)</a>.
Anticipate your customers questions &amp; worries on practical details like shipping rates &amp; policies, return &amp; replacement policies, payment acquirers &amp; security and your <a href="https://www.odoo.com/documentation/user/online/ecommerce/managing_products/stock.html" target="_blank">product's availability (In Stock, Not Available, etc.)</a>.
</p>
</div>
</div>
<div class="alert alert-info info_icon" role="alert">
<span class="fa fa-lightbulb-o fa-2x"></span>
<p>Put the practical details (shipping, payment methods, etc.) as links in the footer; That way, they will be accessible
<p>Put the practical details (shipping, payment acquirers, etc.) as links in the footer; That way, they will be accessible
all your product pages.</p>
</div>
@@ -322,12 +322,12 @@
</t>
<t t-call="web_planner.page">
<t t-set="page_title">Payment Methods</t>
<t t-set="page_title">Payment Acquirers</t>
<p class="text-muted">
To collect payments, you can either request your customers to process it manually (wire transfer, etc.) or redirect them to payment acquirers.<br/>
Using payment acquirers costs you money, so take some time to compare them.
</p>
<h4>You can setup 3 types of <strong>payment methods in Odoo:</strong></h4>
<h4>You can setup 3 types of <strong>payment acquirers in Odoo:</strong></h4>
<div class="row">
<div class="col-md-6 col-xs-12">
<div class="panel panel-success">
@@ -383,11 +383,11 @@
<p>
<strong>Next actions:</strong>
<li><a href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/payment.html" target="blank">See how to configure payment methods</a><br/></li><br/>
<li><a href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/payment.html" target="blank">See how to configure payment acquirers</a><br/></li><br/>
<div class="alert alert-info info_icon" role="alert">
<span class="fa fa-lightbulb-o fa-2x"></span>
<li>Some acquirers like Authorize.net and Ingenico can be used as payment gateways with your own payment processor.</li>
<li><strong>Tip for B2B users:</strong> you can create new manually-processed payment methods (check, purchase order, etc.) by renaming 'Wire Transfer' or duplicating it.</li>
<li><strong>Tip for B2B users:</strong> you can create new manually-processed payment acquirers (check, purchase order, etc.) by renaming 'Wire Transfer' or duplicating it.</li>
</div>
</p>
</div>
+14 -2
View File
@@ -39,6 +39,7 @@ class WebsiteConfigSettings(models.TransientModel):
module_website_sale_wishlist = fields.Boolean("Wishlists ", help='Installs *e-Commerce Wishlist*')
module_website_sale_comparison = fields.Boolean("Product Comparator", help='Installs *e-Commerce Comparator*')
module_account_invoicing = fields.Boolean("Invoicing")
module_sale_stock = fields.Boolean("Delivery Orders")
# the next 2 fields represent sale_pricelist_setting from sale.config.settings, they are split here for the form view, to improve usability
@@ -69,9 +70,10 @@ class WebsiteConfigSettings(models.TransientModel):
group='base.group_portal,base.group_user,base.group_public')
default_invoice_policy = fields.Selection([
('order', 'Ordered quantities'),
('delivery', 'Delivered quantities or service hours')
('order', 'Invoice what is ordered'),
('delivery', 'Invoice what is delivered')
], 'Invoicing Policy', default='order')
automatic_invoice = fields.Boolean("Automatic Invoice")
group_multi_currency = fields.Boolean(string='Multi-Currencies', implied_group='base.group_multi_currency')
@@ -80,6 +82,16 @@ class WebsiteConfigSettings(models.TransientModel):
('subtotal', 'Tax-Excluded Prices')],
"Product Prices", default='total')
@api.multi
def set_automatic_invoice(self):
value = self.module_account_invoicing and self.default_invoice_policy == 'order' and self.automatic_invoice
self.env['ir.config_parameter'].sudo().set_param('website_sale.automatic_invoice', value)
@api.model
def get_default_automatic_invoice(self, fields):
value = self.env['ir.config_parameter'].sudo().get_param('website_sale.automatic_invoice', default=False)
return {'automatic_invoice': value}
@api.model
def get_default_sale_delivery_settings(self, fields):
sale_delivery_settings = 'none'
+24 -309
View File
@@ -3,7 +3,7 @@
import logging
import random
from odoo import api, models, fields, tools, _
from odoo import api, models, fields, _
from odoo.http import request
from odoo.exceptions import UserError, ValidationError
from odoo.tools import pycompat
@@ -23,6 +23,16 @@ class SaleOrder(models.Model):
payment_acquirer_id = fields.Many2one('payment.acquirer', string='Payment Acquirer', copy=False)
payment_tx_id = fields.Many2one('payment.transaction', string='Last Transaction', copy=False)
only_services = fields.Boolean(compute='_compute_cart_info', string='Only Services')
can_directly_mark_as_paid = fields.Boolean(compute='_compute_can_directly_mark_as_paid',
string="Can be directly marked as paid", store=True,
help="""Checked if the sales order can directly be marked as paid, i.e. if the quotation
is sent or confirmed and if the payment acquire is of the type transfer or manual""")
@api.depends('state', 'payment_tx_id', 'payment_tx_id.state',
'payment_acquirer_id', 'payment_acquirer_id.provider')
def _compute_can_directly_mark_as_paid(self):
for order in self:
order.can_directly_mark_as_paid = order.state in ['sent', 'sale'] and order.payment_tx_id and order.payment_acquirer_id.provider in ['transfer', 'manual']
@api.multi
@api.depends('website_order_line.product_uom_qty', 'website_order_line.product_id')
@@ -166,316 +176,21 @@ class SaleOrder(models.Model):
accessory_products -= order.website_order_line.mapped('product_id')
return random.sample(accessory_products, len(accessory_products))
class Website(models.Model):
_inherit = 'website'
pricelist_id = fields.Many2one('product.pricelist', compute='_compute_pricelist_id', string='Default Pricelist')
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Default Currency')
salesperson_id = fields.Many2one('res.users', string='Salesperson')
salesteam_id = fields.Many2one('crm.team', string='Sales Channel')
pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids",
string='Price list available for this Ecommerce/Website')
@api.one
def _compute_pricelist_ids(self):
self.pricelist_ids = self.env["product.pricelist"].search([("website_id", "=", self.id)])
@api.multi
def _compute_pricelist_id(self):
for website in self:
if website._context.get('website_id') != website.id:
website = website.with_context(website_id=website.id)
website.pricelist_id = website.get_current_pricelist()
# This method is cached, must not return records! See also #8795
@tools.ormcache('self.env.uid', 'country_code', 'show_visible', 'website_pl', 'current_pl', 'all_pl', 'partner_pl', 'order_pl')
def _get_pl_partner_order(self, country_code, show_visible, website_pl, current_pl, all_pl, partner_pl=False, order_pl=False):
""" Return the list of pricelists that can be used on website for the current user.
:param str country_code: code iso or False, If set, we search only price list available for this country
:param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo)
:param int website_pl: The default pricelist used on this website
:param int current_pl: The current pricelist used on the website
(If not selectable but the current pricelist we had this pricelist anyway)
:param list all_pl: List of all pricelist available for this website
:param int partner_pl: the partner pricelist
:param int order_pl: the current cart pricelist
:returns: list of pricelist ids
"""
pricelists = self.env['product.pricelist']
if country_code:
for cgroup in self.env['res.country.group'].search([('country_ids.code', '=', country_code)]):
for group_pricelists in cgroup.pricelist_ids:
if not show_visible or group_pricelists.selectable or group_pricelists.id in (current_pl, order_pl):
pricelists |= group_pricelists
partner = self.env.user.partner_id
is_public = self.user_id.id == self.env.user.id
if not is_public and (not pricelists or (partner_pl or partner.property_product_pricelist.id) != website_pl):
if partner.property_product_pricelist.website_id:
pricelists |= partner.property_product_pricelist
if not pricelists: # no pricelist for this country, or no GeoIP
pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl))
else:
pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code)
# This method is cached, must not return records! See also #8795
return pricelists.ids
def _get_pl(self, country_code, show_visible, website_pl, current_pl, all_pl):
pl_ids = self._get_pl_partner_order(country_code, show_visible, website_pl, current_pl, all_pl)
return self.env['product.pricelist'].browse(pl_ids)
def get_pricelist_available(self, show_visible=False):
""" Return the list of pricelists that can be used on website for the current user.
Country restrictions will be detected with GeoIP (if installed).
:param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo)
:returns: pricelist recordset
"""
website = request.website
if not request.website:
if self.env.context.get('website_id'):
website = self.browse(self.env.context['website_id'])
else:
website = self.search([], limit=1)
isocountry = request.session.geoip and request.session.geoip.get('country_code') or False
partner = self.env.user.partner_id
order_pl = partner.last_website_so_id and partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id
partner_pl = partner.property_product_pricelist
pricelists = website._get_pl_partner_order(isocountry, show_visible,
website.user_id.sudo().partner_id.property_product_pricelist.id,
request.session.get('website_sale_current_pl'),
website.pricelist_ids,
partner_pl=partner_pl and partner_pl.id or None,
order_pl=order_pl and order_pl.id or None)
return self.env['product.pricelist'].browse(pricelists)
def is_pricelist_available(self, pl_id):
""" Return a boolean to specify if a specific pricelist can be manually set on the website.
Warning: It check only if pricelist is in the 'selectable' pricelists or the current pricelist.
:param int pl_id: The pricelist id to check
:returns: Boolean, True if valid / available
"""
return pl_id in self.get_pricelist_available(show_visible=False).ids
def get_current_pricelist(self):
"""
:returns: The current pricelist record
"""
# The list of available pricelists for this user.
# If the user is signed in, and has a pricelist set different than the public user pricelist
# then this pricelist will always be considered as available
available_pricelists = self.get_pricelist_available()
pl = None
partner = self.env.user.partner_id
if request.session.get('website_sale_current_pl'):
# `website_sale_current_pl` is set only if the user specifically chose it:
# - Either, he chose it from the pricelist selection
# - Either, he entered a coupon code
pl = self.env['product.pricelist'].browse(request.session['website_sale_current_pl'])
if pl not in available_pricelists:
pl = None
request.session.pop('website_sale_current_pl')
if not pl:
# If the user has a saved cart, it take the pricelist of this cart, except if
# the order is no longer draft (It has already been confirmed, or cancelled, ...)
pl = partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id
if not pl:
# The pricelist of the user set on its partner form.
# If the user is not signed in, it's the public user pricelist
pl = partner.property_product_pricelist
if available_pricelists and pl not in available_pricelists:
# If there is at least one pricelist in the available pricelists
# and the chosen pricelist is not within them
# it then choose the first available pricelist.
# This can only happen when the pricelist is the public user pricelist and this pricelist is not in the available pricelist for this localization
# If the user is signed in, and has a special pricelist (different than the public user pricelist),
# then this special pricelist is amongs these available pricelists, and therefore it won't fall in this case.
pl = available_pricelists[0]
if not pl:
_logger.error('Fail to find pricelist for partner "%s" (id %s)', partner.name, partner.id)
return pl
@api.multi
def sale_product_domain(self):
return [("sale_ok", "=", True)]
@api.model
def sale_get_payment_term(self, partner):
DEFAULT_PAYMENT_TERM = 'account.account_payment_term_immediate'
return self.env.ref(DEFAULT_PAYMENT_TERM, False).id or partner.property_payment_term_id.id
@api.multi
def _prepare_sale_order_values(self, partner, pricelist):
def action_mark_as_paid(self):
""" Mark directly a sales order as paid if:
- State: Quotation Sent, or sales order
- Provider: wire transfer or manual config
The transaction is marked as done
The invoice may be generated and marked as paid if configured in the website settings
"""
self.ensure_one()
affiliate_id = request.session.get('affiliate_id')
salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id
addr = partner.address_get(['delivery', 'invoice'])
values = {
'partner_id': partner.id,
'pricelist_id': pricelist.id,
'payment_term_id': self.sale_get_payment_term(partner),
'team_id': self.salesteam_id.id,
'partner_invoice_id': addr['invoice'],
'partner_shipping_id': addr['delivery'],
'user_id': salesperson_id or self.salesperson_id.id,
}
company = self.company_id or pricelist.company_id
if company:
values['company_id'] = company.id
return values
@api.multi
def sale_get_order(self, force_create=False, code=None, update_pricelist=False, force_pricelist=False):
""" Return the current sales order after mofications specified by params.
:param bool force_create: Create sales order if not already existing
:param str code: Code to force a pricelist (promo code)
If empty, it's a special case to reset the pricelist with the first available else the default.
:param bool update_pricelist: Force to recompute all the lines from sales order to adapt the price with the current pricelist.
:param int force_pricelist: pricelist_id - if set, we change the pricelist with this one
:returns: browse record for the current sales order
"""
self.ensure_one()
partner = self.env.user.partner_id
sale_order_id = request.session.get('sale_order_id')
if not sale_order_id:
last_order = partner.last_website_so_id
available_pricelists = self.get_pricelist_available()
# Do not reload the cart of this user last visit if the cart is no longer draft or uses a pricelist no longer available.
sale_order_id = last_order.state == 'draft' and last_order.pricelist_id in available_pricelists and last_order.id
pricelist_id = request.session.get('website_sale_current_pl') or self.get_current_pricelist().id
if self.env['product.pricelist'].browse(force_pricelist).exists():
pricelist_id = force_pricelist
request.session['website_sale_current_pl'] = pricelist_id
update_pricelist = True
if not self._context.get('pricelist'):
self = self.with_context(pricelist=pricelist_id)
# Test validity of the sale_order_id
sale_order = self.env['sale.order'].sudo().browse(sale_order_id).exists() if sale_order_id else None
# create so if needed
if not sale_order and (force_create or code):
# TODO cache partner_id session
pricelist = self.env['product.pricelist'].browse(pricelist_id).sudo()
so_data = self._prepare_sale_order_values(partner, pricelist)
sale_order = self.env['sale.order'].sudo().create(so_data)
# set fiscal position
if request.website.partner_id.id != partner.id:
sale_order.onchange_partner_shipping_id()
else: # For public user, fiscal position based on geolocation
country_code = request.session['geoip'].get('country_code')
if country_code:
country_id = request.env['res.country'].search([('code', '=', country_code)], limit=1).id
fp_id = request.env['account.fiscal.position'].sudo()._get_fpos_by_region(country_id)
sale_order.fiscal_position_id = fp_id
else:
# if no geolocation, use the public user fp
sale_order.onchange_partner_shipping_id()
request.session['sale_order_id'] = sale_order.id
if request.website.partner_id.id != partner.id:
partner.write({'last_website_so_id': sale_order.id})
if sale_order:
# case when user emptied the cart
if not request.session.get('sale_order_id'):
request.session['sale_order_id'] = sale_order.id
# check for change of pricelist with a coupon
pricelist_id = pricelist_id or partner.property_product_pricelist.id
# check for change of partner_id ie after signup
if sale_order.partner_id.id != partner.id and request.website.partner_id.id != partner.id:
flag_pricelist = False
if pricelist_id != sale_order.pricelist_id.id:
flag_pricelist = True
fiscal_position = sale_order.fiscal_position_id.id
# change the partner, and trigger the onchange
sale_order.write({'partner_id': partner.id})
sale_order.onchange_partner_id()
sale_order.onchange_partner_shipping_id() # fiscal position
sale_order['payment_term_id'] = self.sale_get_payment_term(partner)
# check the pricelist : update it if the pricelist is not the 'forced' one
values = {}
if sale_order.pricelist_id:
if sale_order.pricelist_id.id != pricelist_id:
values['pricelist_id'] = pricelist_id
update_pricelist = True
# if fiscal position, update the order lines taxes
if sale_order.fiscal_position_id:
sale_order._compute_tax_id()
# if values, then make the SO update
if values:
sale_order.write(values)
# check if the fiscal position has changed with the partner_id update
recent_fiscal_position = sale_order.fiscal_position_id.id
if flag_pricelist or recent_fiscal_position != fiscal_position:
update_pricelist = True
if code and code != sale_order.pricelist_id.code:
code_pricelist = self.env['product.pricelist'].sudo().search([('code', '=', code)], limit=1)
if code_pricelist:
pricelist_id = code_pricelist.id
update_pricelist = True
elif code is not None and sale_order.pricelist_id.code:
# code is not None when user removes code and click on "Apply"
pricelist_id = partner.property_product_pricelist.id
update_pricelist = True
# update the pricelist
if update_pricelist:
request.session['website_sale_current_pl'] = pricelist_id
values = {'pricelist_id': pricelist_id}
sale_order.write(values)
for line in sale_order.order_line:
if line.exists():
sale_order._cart_update(product_id=line.product_id.id, line_id=line.id, add_qty=0)
if self.can_directly_mark_as_paid:
self.action_confirm()
if self.env['ir.config_parameter'].sudo().get_param('website_sale.automatic_invoice', default=False):
self.payment_tx_id._generate_and_pay_invoice(self.payment_tx_id, self.payment_acquirer_id.provider)
self.payment_tx_id.state = 'done'
else:
request.session['sale_order_id'] = None
return None
return sale_order
def sale_get_transaction(self):
tx_id = request.session.get('sale_transaction_id')
if tx_id:
transaction = self.env['payment.transaction'].sudo().browse(tx_id)
# Ugly hack for SIPS: SIPS does not allow to reuse a payment reference, even if the
# payment was not not proceeded. For example:
# - Select SIPS for payment
# - Be redirected to SIPS website
# - Go back to eCommerce without paying
# - Be redirected to SIPS website again => error
# Since there is no link module between 'website_sale' and 'payment_sips', we prevent
# here to reuse any previous transaction for SIPS.
if transaction.state != 'cancel' and transaction.acquirer_id.provider != 'sips':
return transaction
else:
request.session['sale_transaction_id'] = False
return False
def sale_reset(self):
request.session.update({
'sale_order_id': False,
'sale_transaction_id': False,
'website_sale_current_pl': False,
})
raise ValidationError(_("The quote should be sent and the payment acquirer type should be manual or wire transfer"))
class ResCountry(models.Model):
+314 -1
View File
@@ -1,12 +1,325 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
import logging
from odoo import api, fields, models, tools
from odoo.http import request
_logger = logging.getLogger(__name__)
class Website(models.Model):
_inherit = 'website'
pricelist_id = fields.Many2one('product.pricelist', compute='_compute_pricelist_id', string='Default Pricelist')
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Default Currency')
salesperson_id = fields.Many2one('res.users', string='Salesperson')
salesteam_id = fields.Many2one('crm.team', string='Sales Channel')
pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids",
string='Price list available for this Ecommerce/Website')
@api.one
def _compute_pricelist_ids(self):
self.pricelist_ids = self.env["product.pricelist"].search([("website_id", "=", self.id)])
@api.multi
def _compute_pricelist_id(self):
for website in self:
if website._context.get('website_id') != website.id:
website = website.with_context(website_id=website.id)
website.pricelist_id = website.get_current_pricelist()
# This method is cached, must not return records! See also #8795
@tools.ormcache('self.env.uid', 'country_code', 'show_visible', 'website_pl', 'current_pl', 'all_pl', 'partner_pl', 'order_pl')
def _get_pl_partner_order(self, country_code, show_visible, website_pl, current_pl, all_pl, partner_pl=False, order_pl=False):
""" Return the list of pricelists that can be used on website for the current user.
:param str country_code: code iso or False, If set, we search only price list available for this country
:param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo)
:param int website_pl: The default pricelist used on this website
:param int current_pl: The current pricelist used on the website
(If not selectable but the current pricelist we had this pricelist anyway)
:param list all_pl: List of all pricelist available for this website
:param int partner_pl: the partner pricelist
:param int order_pl: the current cart pricelist
:returns: list of pricelist ids
"""
pricelists = self.env['product.pricelist']
if country_code:
for cgroup in self.env['res.country.group'].search([('country_ids.code', '=', country_code)]):
for group_pricelists in cgroup.pricelist_ids:
if not show_visible or group_pricelists.selectable or group_pricelists.id in (current_pl, order_pl):
pricelists |= group_pricelists
partner = self.env.user.partner_id
is_public = self.user_id.id == self.env.user.id
if not is_public and (not pricelists or (partner_pl or partner.property_product_pricelist.id) != website_pl):
if partner.property_product_pricelist.website_id:
pricelists |= partner.property_product_pricelist
if not pricelists: # no pricelist for this country, or no GeoIP
pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl))
else:
pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code)
# This method is cached, must not return records! See also #8795
return pricelists.ids
def _get_pl(self, country_code, show_visible, website_pl, current_pl, all_pl):
pl_ids = self._get_pl_partner_order(country_code, show_visible, website_pl, current_pl, all_pl)
return self.env['product.pricelist'].browse(pl_ids)
def get_pricelist_available(self, show_visible=False):
""" Return the list of pricelists that can be used on website for the current user.
Country restrictions will be detected with GeoIP (if installed).
:param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo)
:returns: pricelist recordset
"""
website = request.website
if not request.website:
if self.env.context.get('website_id'):
website = self.browse(self.env.context['website_id'])
else:
website = self.search([], limit=1)
isocountry = request.session.geoip and request.session.geoip.get('country_code') or False
partner = self.env.user.partner_id
order_pl = partner.last_website_so_id and partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id
partner_pl = partner.property_product_pricelist
pricelists = website._get_pl_partner_order(isocountry, show_visible,
website.user_id.sudo().partner_id.property_product_pricelist.id,
request.session.get('website_sale_current_pl'),
website.pricelist_ids,
partner_pl=partner_pl and partner_pl.id or None,
order_pl=order_pl and order_pl.id or None)
return self.env['product.pricelist'].browse(pricelists)
def is_pricelist_available(self, pl_id):
""" Return a boolean to specify if a specific pricelist can be manually set on the website.
Warning: It check only if pricelist is in the 'selectable' pricelists or the current pricelist.
:param int pl_id: The pricelist id to check
:returns: Boolean, True if valid / available
"""
return pl_id in self.get_pricelist_available(show_visible=False).ids
def get_current_pricelist(self):
"""
:returns: The current pricelist record
"""
# The list of available pricelists for this user.
# If the user is signed in, and has a pricelist set different than the public user pricelist
# then this pricelist will always be considered as available
available_pricelists = self.get_pricelist_available()
pl = None
partner = self.env.user.partner_id
if request.session.get('website_sale_current_pl'):
# `website_sale_current_pl` is set only if the user specifically chose it:
# - Either, he chose it from the pricelist selection
# - Either, he entered a coupon code
pl = self.env['product.pricelist'].browse(request.session['website_sale_current_pl'])
if pl not in available_pricelists:
pl = None
request.session.pop('website_sale_current_pl')
if not pl:
# If the user has a saved cart, it take the pricelist of this cart, except if
# the order is no longer draft (It has already been confirmed, or cancelled, ...)
pl = partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id
if not pl:
# The pricelist of the user set on its partner form.
# If the user is not signed in, it's the public user pricelist
pl = partner.property_product_pricelist
if available_pricelists and pl not in available_pricelists:
# If there is at least one pricelist in the available pricelists
# and the chosen pricelist is not within them
# it then choose the first available pricelist.
# This can only happen when the pricelist is the public user pricelist and this pricelist is not in the available pricelist for this localization
# If the user is signed in, and has a special pricelist (different than the public user pricelist),
# then this special pricelist is amongs these available pricelists, and therefore it won't fall in this case.
pl = available_pricelists[0]
if not pl:
_logger.error('Fail to find pricelist for partner "%s" (id %s)', partner.name, partner.id)
return pl
@api.multi
def sale_product_domain(self):
return [("sale_ok", "=", True)]
@api.model
def sale_get_payment_term(self, partner):
DEFAULT_PAYMENT_TERM = 'account.account_payment_term_immediate'
return self.env.ref(DEFAULT_PAYMENT_TERM, False).id or partner.property_payment_term_id.id
@api.multi
def _prepare_sale_order_values(self, partner, pricelist):
self.ensure_one()
affiliate_id = request.session.get('affiliate_id')
salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id
addr = partner.address_get(['delivery', 'invoice'])
values = {
'partner_id': partner.id,
'pricelist_id': pricelist.id,
'payment_term_id': self.sale_get_payment_term(partner),
'team_id': self.salesteam_id.id,
'partner_invoice_id': addr['invoice'],
'partner_shipping_id': addr['delivery'],
'user_id': salesperson_id or self.salesperson_id.id,
}
company = self.company_id or pricelist.company_id
if company:
values['company_id'] = company.id
return values
@api.multi
def sale_get_order(self, force_create=False, code=None, update_pricelist=False, force_pricelist=False):
""" Return the current sales order after mofications specified by params.
:param bool force_create: Create sales order if not already existing
:param str code: Code to force a pricelist (promo code)
If empty, it's a special case to reset the pricelist with the first available else the default.
:param bool update_pricelist: Force to recompute all the lines from sales order to adapt the price with the current pricelist.
:param int force_pricelist: pricelist_id - if set, we change the pricelist with this one
:returns: browse record for the current sales order
"""
self.ensure_one()
partner = self.env.user.partner_id
sale_order_id = request.session.get('sale_order_id')
if not sale_order_id:
last_order = partner.last_website_so_id
available_pricelists = self.get_pricelist_available()
# Do not reload the cart of this user last visit if the cart is no longer draft or uses a pricelist no longer available.
sale_order_id = last_order.state == 'draft' and last_order.pricelist_id in available_pricelists and last_order.id
pricelist_id = request.session.get('website_sale_current_pl') or self.get_current_pricelist().id
if self.env['product.pricelist'].browse(force_pricelist).exists():
pricelist_id = force_pricelist
request.session['website_sale_current_pl'] = pricelist_id
update_pricelist = True
if not self._context.get('pricelist'):
self = self.with_context(pricelist=pricelist_id)
# Test validity of the sale_order_id
sale_order = self.env['sale.order'].sudo().browse(sale_order_id).exists() if sale_order_id else None
# create so if needed
if not sale_order and (force_create or code):
# TODO cache partner_id session
pricelist = self.env['product.pricelist'].browse(pricelist_id).sudo()
so_data = self._prepare_sale_order_values(partner, pricelist)
sale_order = self.env['sale.order'].sudo().create(so_data)
# set fiscal position
if request.website.partner_id.id != partner.id:
sale_order.onchange_partner_shipping_id()
else: # For public user, fiscal position based on geolocation
country_code = request.session['geoip'].get('country_code')
if country_code:
country_id = request.env['res.country'].search([('code', '=', country_code)], limit=1).id
fp_id = request.env['account.fiscal.position'].sudo()._get_fpos_by_region(country_id)
sale_order.fiscal_position_id = fp_id
else:
# if no geolocation, use the public user fp
sale_order.onchange_partner_shipping_id()
request.session['sale_order_id'] = sale_order.id
if request.website.partner_id.id != partner.id:
partner.write({'last_website_so_id': sale_order.id})
if sale_order:
# case when user emptied the cart
if not request.session.get('sale_order_id'):
request.session['sale_order_id'] = sale_order.id
# check for change of pricelist with a coupon
pricelist_id = pricelist_id or partner.property_product_pricelist.id
# check for change of partner_id ie after signup
if sale_order.partner_id.id != partner.id and request.website.partner_id.id != partner.id:
flag_pricelist = False
if pricelist_id != sale_order.pricelist_id.id:
flag_pricelist = True
fiscal_position = sale_order.fiscal_position_id.id
# change the partner, and trigger the onchange
sale_order.write({'partner_id': partner.id})
sale_order.onchange_partner_id()
sale_order.onchange_partner_shipping_id() # fiscal position
sale_order['payment_term_id'] = self.sale_get_payment_term(partner)
# check the pricelist : update it if the pricelist is not the 'forced' one
values = {}
if sale_order.pricelist_id:
if sale_order.pricelist_id.id != pricelist_id:
values['pricelist_id'] = pricelist_id
update_pricelist = True
# if fiscal position, update the order lines taxes
if sale_order.fiscal_position_id:
sale_order._compute_tax_id()
# if values, then make the SO update
if values:
sale_order.write(values)
# check if the fiscal position has changed with the partner_id update
recent_fiscal_position = sale_order.fiscal_position_id.id
if flag_pricelist or recent_fiscal_position != fiscal_position:
update_pricelist = True
if code and code != sale_order.pricelist_id.code:
code_pricelist = self.env['product.pricelist'].sudo().search([('code', '=', code)], limit=1)
if code_pricelist:
pricelist_id = code_pricelist.id
update_pricelist = True
elif code is not None and sale_order.pricelist_id.code:
# code is not None when user removes code and click on "Apply"
pricelist_id = partner.property_product_pricelist.id
update_pricelist = True
# update the pricelist
if update_pricelist:
request.session['website_sale_current_pl'] = pricelist_id
values = {'pricelist_id': pricelist_id}
sale_order.write(values)
for line in sale_order.order_line:
if line.exists():
sale_order._cart_update(product_id=line.product_id.id, line_id=line.id, add_qty=0)
else:
request.session['sale_order_id'] = None
return None
return sale_order
def sale_get_transaction(self):
tx_id = request.session.get('sale_transaction_id')
if tx_id:
transaction = self.env['payment.transaction'].sudo().browse(tx_id)
# Ugly hack for SIPS: SIPS does not allow to reuse a payment reference, even if the
# payment was not not proceeded. For example:
# - Select SIPS for payment
# - Be redirected to SIPS website
# - Go back to eCommerce without paying
# - Be redirected to SIPS website again => error
# Since there is no link module between 'website_sale' and 'payment_sips', we prevent
# here to reuse any previous transaction for SIPS.
if transaction.state != 'cancel' and transaction.acquirer_id.provider != 'sips':
return transaction
else:
request.session['sale_transaction_id'] = False
return False
def sale_reset(self):
request.session.update({
'sale_order_id': False,
'sale_transaction_id': False,
'website_sale_current_pl': False,
})
@api.model
def action_dashboard_redirect(self):
if self.env.user.has_group('sales_team.group_sale_salesman'):
@@ -2,20 +2,13 @@
<templates>
<t t-extend="website.dashboard_header">
<t t-jquery="t[t-foreach='apps']" t-operation="before">
<div t-if="widget.dashboards_data.sales.summary.order_unpaid_count" class="o_inner_box o_main o_box_item" title="Confirm orders when you get paid.">
<div t-if="widget.dashboards_data.sales.summary.order_unpaid_count" class="o_inner_box o_main o_box_item" title="Confirm orders when you get paid." t-name='unpaid_orders_box'>
<a href="#" class="o_dashboard_action" name="website_sale.action_unpaid_orders_ecommerce">
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_unpaid_count"/></div>
Unpaid Orders
</a>
<t t-set="vignette_nbr" t-value="vignette_nbr + 1"/>
</div>
<div t-if="widget.dashboards_data.sales.summary.order_to_invoice_count" class="o_inner_box o_main o_box_item" title="Generate an invoice from orders ready for invoicing.">
<a href="#" class="o_dashboard_action" name="website_sale.sale_order_action_to_invoice">
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_to_invoice_count"/></div>
Orders to Invoice
</a>
<t t-set="vignette_nbr" t-value="vignette_nbr + 1"/>
</div>
<div t-if="widget.dashboards_data.sales.summary.payment_to_capture_count" class="o_inner_box o_main o_box_item" title="Capture order payments when the delivery is completed.">
<a href="#" class="o_dashboard_action" name="website_sale.payment_transaction_action_payments_to_capture">
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.payment_to_capture_count"/></div>
@@ -36,8 +36,7 @@ class TestWebsitePriceList(TransactionCase):
'country_group_ids': [(6, 0, [ca_group.id])],
'sequence': 10
})
import odoo.addons.website_sale.models.sale_order
patcher = patch('odoo.addons.website_sale.models.sale_order.Website.get_pricelist_available', wraps=self._get_pricelist_available)
patcher = patch('odoo.addons.website_sale.models.website.Website.get_pricelist_available', wraps=self._get_pricelist_available)
patcher.start()
self.addCleanup(patcher.stop)
@@ -13,12 +13,9 @@
<tbody class="sale_tbody">
<tr>
<td>
<strong>Payment Method:</strong> <t t-esc="doc.payment_acquirer_id.name"/><br />
<strong>Payment Acquirer:</strong> <t t-esc="doc.payment_acquirer_id.name"/><br />
<strong>Payment Status:</strong>
<t t-if="doc.payment_acquirer_id.auto_confirm == 'none'">
<t t-raw="doc.payment_acquirer_id.pending_msg"/>
</t>
<t t-if="doc.payment_acquirer_id.auto_confirm in ['confirm_so', 'generate_and_pay_invoice']">
<t t-if="doc.payment_acquirer_id.journal_id">
<t t-if="doc.payment_tx_id.state == 'pending'">
<t t-raw="doc.payment_acquirer_id.pending_msg"/>
</t>
+18 -5
View File
@@ -220,20 +220,33 @@
</div>
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container" id="sale_invoicing_settings">
<div class="col-xs-12 col-md-6 o_setting_box" id="invoicing_policy_setting" title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div class="o_setting_left_pane"/>
<div class="col-xs-12 col-md-6 o_setting_box" id="invoicing_policy_setting" title="Choose the automatic mode if you use to issue an invoice whenever an order is placed. The manual mode allows you to invoice based on what is really delivered. That way you can invoice in several steps, etc. The mode selected here applies to any new product created but not to products already existing. If you want to invoice delivered quantities, you must update the invoicing policy of those products.">
<div class="o_setting_left_pane">
<field name="module_account_invoicing" nolabel="1"/>
</div>
<div class="o_setting_right_pane">
<label for="default_invoice_policy"/>
<label for="module_account_invoicing"/>
<div class="text-muted">
Quantities to invoice from sales orders
Issue invoices to customers
</div>
<div class="content-group">
<div class="content-group" attrs="{'invisible': [('module_account_invoicing', '=', False)]}">
<div class="mt16">
<field name="default_invoice_policy" class="o_light_label" widget="radio"/>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" attrs="{'invisible': ['|', ('default_invoice_policy', '=', 'delivery'), ('module_account_invoicing', '=', False)]}">
<div class="o_setting_left_pane">
<field name="automatic_invoice" nolabel="1"/>
</div>
<div class="o_setting_right_pane">
<label for="automatic_invoice"/>
<div class="text-muted">
Issue the invoice automatically at the order confirmation
</div>
</div>
</div>
</div>
<h2>Shipping</h2>
<div class="row mt16 o_settings_container" id="sale_shipping_settings">
+18 -13
View File
@@ -13,18 +13,6 @@
</field>
</record>
<record id="view_order_form_acquirer" model="ir.ui.view">
<field name="name">sale.order.form.acquirer</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount_total']" position="after">
<field name="payment_acquirer_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}" options='{"no_open":True}'/>
<field name="payment_tx_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}"/>
</xpath>
</field>
</record>
<record id="action_orders_ecommerce" model="ir.actions.act_window">
<field name="name">Orders</field>
<field name="res_model">sale.order</field>
@@ -125,7 +113,7 @@
<!-- Configuration sub-menus -->
<menuitem id="menu_ecommerce_payment_acquirers"
action="payment.action_payment_acquirer"
parent="menu_ecommerce_settings" name="Payment Methods" />
parent="menu_ecommerce_settings" name="Payment Acquirers" />
<menuitem id="menu_product_attribute_action"
action="product.attribute_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="1"/>
@@ -133,4 +121,21 @@
action="product.variants_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="2"/>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_quotation_send']" position="after">
<button name="action_mark_as_paid" type="object" string="Mark as Paid" states="sent,sale" class="btn-primary" attrs="{'invisible': [('can_directly_mark_as_paid', '=', False)]}"/>
</xpath>
<field name="partner_id" position="after">
<field name="can_directly_mark_as_paid" invisible="1"/>
</field>
<xpath expr="//button[@id='action_confirm']" position="attributes">
<attribute name="attrs">{'invisible': [('can_directly_mark_as_paid', '=', True)]}</attribute>
</xpath>
</field>
</record>
</odoo>
+3 -3
View File
@@ -1294,7 +1294,7 @@
</template>
<template id="payment" name="Payment">
<t t-call="website.layout">
<t t-set="additional_title">Shop - Select Payment Method</t>
<t t-set="additional_title">Shop - Select Payment Acquirer</t>
<t t-set="no_footer">1</t>
<div id="wrap">
@@ -1375,7 +1375,7 @@
<div class="oe_structure" />
<div class="js_payment mb64 col-md-12 row" t-if="acquirers and website_sale_order.amount_total" id="payment_method">
<div class="col-lg-7 col-sm-8">
<h4>Choose a payment method</h4>
<h4>Choose a payment acquirer</h4>
<ul class="list-unstyled">
<t t-set="payment_methods_available" t-value="False"/>
<t t-set="partner_country" t-value="website_sale_order and website_sale_order.partner_invoice_id.country_id"/>
@@ -1419,7 +1419,7 @@
<a class="btn btn-default btn-block"
groups="base.group_system"
t-attf-href="/web#return_label=Website&amp;action=#{payment_action_id}">
<i class="fa fa-plus-square" aria-hidden="true"></i> Setup payment methods
<i class="fa fa-plus-square" aria-hidden="true"></i> Setup payment acquirers
</a>
</div>
<div class="col-lg-5 col-sm-4 text-right">
@@ -0,0 +1 @@
# -*- encoding: utf-8 -*-
@@ -0,0 +1,20 @@
# -*- encoding: utf-8 -*-
{
'name': 'Website Sale - Sale Management',
'version': '1.0',
'category': 'Website',
'description': """
Display orders to invoice in website dashboard.
""",
'depends': [
'sale_management',
'website_sale',
],
'installable': True,
'autoinstall': True,
'data': [
],
'demo': [
],
'qweb': ['static/src/xml/*.xml'],
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="website.dashboard_header">
<t t-jquery=".o_dashboard_common a.o_dashboard_action .o_primary" t-operation="after">
<div t-if="widget.dashboards_data.sales.summary.order_to_invoice_count" class="o_inner_box o_main o_box_item" title="Generate an invoice from orders ready for invoicing.">
<a href="#" class="o_dashboard_action" name="website_sale.sale_order_action_to_invoice">
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_to_invoice_count"/></div>
Orders to Invoice
</a>
<t t-set="vignette_nbr" t-value="vignette_nbr + 1"/>
</div>
</t>
</t>
</templates>
+1
View File
@@ -239,6 +239,7 @@
<rng:optional> <rng:attribute name="action"/> </rng:optional>
<rng:optional> <rng:attribute name="sequence"/> </rng:optional>
<rng:optional> <rng:attribute name="groups"/> </rng:optional>
<rng:optional> <rng:attribute name="active"/> </rng:optional>
<!-- TODO: remove icon stuff -->
<rng:optional> <rng:attribute name="icon"/> </rng:optional>
<rng:optional> <rng:attribute name="web_icon"/> </rng:optional>
+2
View File
@@ -516,6 +516,8 @@ form: module.record_id""" % (xml_id,)
if rec.get('sequence'):
values['sequence'] = int(rec.get('sequence'))
values['active'] = self.nodeattr2bool(rec, 'active', default=True)
if rec.get('groups'):
g_names = rec.get('groups','').split(',')
groups_value = []