Начальное наполнение
This commit is contained in:
parent
05249d6b97
commit
8109ee6c8c
100
README.md
100
README.md
@ -1,2 +1,100 @@
|
||||
# sale
|
||||
Sales Management Made Easy
|
||||
--------------------------
|
||||
|
||||
From quotes to invoices, in just a few clicks with the Odoo <a href="https://www.odoo.com/app/crm">Sales Management</a>.
|
||||
|
||||
Drive your sales operations from quotes to invoices with all the information
|
||||
you need, easily accessible. Keep track of long term contracts, automate
|
||||
invoicing and notify sales when they have things to do.
|
||||
|
||||
Create Professional Quotations
|
||||
------------------------------
|
||||
|
||||
Create quotations in a matter of seconds. Send quotes by email or get a
|
||||
professional PDF. Track quotations, and convert them to sales order in one
|
||||
click.
|
||||
|
||||
Spend the extra time focusing on selling, not recording data.
|
||||
|
||||
Fully Integrated
|
||||
----------------
|
||||
|
||||
The information your need, where you need it.
|
||||
|
||||
Don't lose time looking for customers, products or contracts related
|
||||
information; they are all conveniently accessible when creating quotations.
|
||||
|
||||
Get access to stock availabilities in the different warehouses, to customer's
|
||||
specific prices, to the history of preceeding offers for this prospect, etc.
|
||||
|
||||
|
||||
Your Address Book
|
||||
-----------------
|
||||
|
||||
So many features, so easy to use.
|
||||
|
||||
Assign tags to your prospects, manage
|
||||
relationships between contacts and store all customer's preferences including
|
||||
pricing, billing conditions, addresses, payment terms, etc.
|
||||
|
||||
Navigate through all the documents related to a customer with the powerfull
|
||||
breadcrumb: quotations, invoices, emails, meetings.
|
||||
|
||||
Fully Integrated Invoicing
|
||||
--------------------------
|
||||
|
||||
Whether you invoice based on time and materials, on delivery orders or fixed
|
||||
price; Odoo supports all possible methods.
|
||||
|
||||
Get recurring invoices produced automatically, create advances in just a few
|
||||
clicks, re-invoices expenses easily, etc.
|
||||
|
||||
Keep track of your contracts
|
||||
----------------------------
|
||||
|
||||
Get rid of wasted paper and record all your contracts in the application.
|
||||
Invoices are generated automatically based on your contract conditions. Your
|
||||
account managers get alerts before contracts have to be renewed.
|
||||
|
||||
Communicate Efficiently With Customers
|
||||
--------------------------------------
|
||||
|
||||
The chatter feature enables you to communicate faster and more efficiently with
|
||||
your customer. This takes place directly on a quotation or sale order from
|
||||
within Odoo or via email.
|
||||
|
||||
Get all the negotiations and discussions attached to the right document and
|
||||
relevent managers notified on specific events.
|
||||
|
||||
Fully Extensible
|
||||
----------------
|
||||
|
||||
By default, sales order are very simple, limited to a small number of features.
|
||||
Don't be confused by features you don't need.
|
||||
|
||||
But you can activate options to fit your specific need: multi-warehouses, multi
|
||||
unit of measures, manage customer specific prices with pricelists, control
|
||||
margins on quotations, use different addresses for shipping and billing, etc.
|
||||
|
||||
Built-in Customer Relationship Management
|
||||
-----------------------------------------
|
||||
|
||||
Activate the CRM application to manage your funnel of opportunities, attract
|
||||
leads, log calls, schedule meetings and launch marketing campaigns.
|
||||
|
||||
Opportunities can be converted into quotations in just one click.
|
||||
|
||||
Drive Engagement with Gamification
|
||||
----------------------------------
|
||||
|
||||
Align Sales Teams on clear targets. Define clear commission plans. Get real
|
||||
time statistics on the performance of individual sales or channels. Motivate your
|
||||
teams with challenges, rewards and leaderboards.
|
||||
|
||||
Have Clear Pricing Strategies
|
||||
-----------------------------
|
||||
|
||||
Use pricelists to record special conditions for a specific customer or to
|
||||
define prices for a segment of customers. Define promotions and have them
|
||||
applied automatically for all your Sales Teams.
|
||||
|
||||
|
15
__init__.py
Normal file
15
__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
from . import report
|
||||
from . import wizard
|
||||
from . import populate
|
||||
|
||||
|
||||
def _synchronize_cron(env):
|
||||
send_invoice_cron = env.ref('sale.send_invoice_cron', raise_if_not_found=False)
|
||||
if send_invoice_cron:
|
||||
config = env['ir.config_parameter'].get_param('sale.automatic_invoice', False)
|
||||
send_invoice_cron.active = bool(config)
|
94
__manifest__.py
Normal file
94
__manifest__.py
Normal file
@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
{
|
||||
'name': 'Sales',
|
||||
'version': '1.2',
|
||||
'category': 'Sales/Sales',
|
||||
'summary': 'Sales internal machinery',
|
||||
'description': """
|
||||
This module contains all the common features of Sales Management and eCommerce.
|
||||
""",
|
||||
'depends': [
|
||||
'sales_team',
|
||||
'account_payment', # -> account, payment, portal
|
||||
'utm',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/res_groups.xml',
|
||||
'security/ir_rules.xml',
|
||||
|
||||
'report/account_invoice_report_views.xml',
|
||||
'report/ir_actions_report_templates.xml',
|
||||
'report/ir_actions_report.xml',
|
||||
'report/sale_report_views.xml',
|
||||
|
||||
'data/ir_cron.xml',
|
||||
'data/ir_sequence_data.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/ir_config_parameter.xml', # Needs mail_template_data
|
||||
'data/onboarding_data.xml',
|
||||
|
||||
'wizard/account_accrued_orders_wizard_views.xml',
|
||||
'wizard/mass_cancel_orders_views.xml',
|
||||
'wizard/payment_link_wizard_views.xml',
|
||||
'wizard/res_config_settings_views.xml',
|
||||
'wizard/sale_make_invoice_advance_views.xml',
|
||||
'wizard/sale_order_cancel_views.xml',
|
||||
'wizard/sale_order_discount_views.xml',
|
||||
|
||||
# Define sale order views before their references
|
||||
'views/sale_order_views.xml',
|
||||
|
||||
'views/account_views.xml',
|
||||
'views/crm_team_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
'views/mail_activity_plan_views.xml',
|
||||
'views/payment_views.xml',
|
||||
'views/product_document_views.xml',
|
||||
'views/product_packaging_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/sale_order_line_views.xml',
|
||||
'views/sale_portal_templates.xml',
|
||||
'views/utm_campaign_views.xml',
|
||||
|
||||
'views/sale_menus.xml', # Last because referencing actions defined in previous files
|
||||
],
|
||||
'demo': [
|
||||
'data/product_demo.xml',
|
||||
'data/sale_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'sale/static/src/scss/sale_onboarding.scss',
|
||||
'sale/static/src/js/sale_progressbar_field.js',
|
||||
'sale/static/src/js/tours/sale.js',
|
||||
'sale/static/src/js/sale_product_field.js',
|
||||
'sale/static/src/xml/**/*',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'sale/static/src/scss/sale_portal.scss',
|
||||
'sale/static/src/js/sale_portal_sidebar.js',
|
||||
'sale/static/src/js/sale_portal_prepayment.js',
|
||||
'sale/static/src/js/sale_portal.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'sale/static/tests/tours/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'sale/static/tests/**/*',
|
||||
('remove', 'sale/static/tests/tours/**/*')
|
||||
],
|
||||
'web.report_assets_common': [
|
||||
'sale/static/src/scss/sale_report.scss',
|
||||
],
|
||||
},
|
||||
'post_init_hook': '_synchronize_cron',
|
||||
'license': 'LGPL-3',
|
||||
}
|
4
controllers/__init__.py
Normal file
4
controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import portal
|
458
controllers/portal.py
Normal file
458
controllers/portal.py
Normal file
@ -0,0 +1,458 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import binascii
|
||||
|
||||
from odoo import fields, http, _
|
||||
from odoo.exceptions import AccessError, MissingError, ValidationError
|
||||
from odoo.fields import Command
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.payment.controllers import portal as payment_portal
|
||||
from odoo.addons.portal.controllers.mail import _message_post_helper
|
||||
from odoo.addons.portal.controllers.portal import pager as portal_pager
|
||||
|
||||
|
||||
class CustomerPortal(payment_portal.PaymentPortal):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super()._prepare_home_portal_values(counters)
|
||||
partner = request.env.user.partner_id
|
||||
|
||||
SaleOrder = request.env['sale.order']
|
||||
if 'quotation_count' in counters:
|
||||
values['quotation_count'] = SaleOrder.search_count(self._prepare_quotations_domain(partner)) \
|
||||
if SaleOrder.check_access_rights('read', raise_exception=False) else 0
|
||||
if 'order_count' in counters:
|
||||
values['order_count'] = SaleOrder.search_count(self._prepare_orders_domain(partner), limit=1) \
|
||||
if SaleOrder.check_access_rights('read', raise_exception=False) else 0
|
||||
|
||||
return values
|
||||
|
||||
def _prepare_quotations_domain(self, partner):
|
||||
return [
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('state', '=', 'sent')
|
||||
]
|
||||
|
||||
def _prepare_orders_domain(self, partner):
|
||||
return [
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('state', '=', 'sale'),
|
||||
]
|
||||
|
||||
def _get_sale_searchbar_sortings(self):
|
||||
return {
|
||||
'date': {'label': _('Order Date'), 'order': 'date_order desc'},
|
||||
'name': {'label': _('Reference'), 'order': 'name'},
|
||||
'stage': {'label': _('Stage'), 'order': 'state'},
|
||||
}
|
||||
|
||||
def _prepare_sale_portal_rendering_values(
|
||||
self, page=1, date_begin=None, date_end=None, sortby=None, quotation_page=False, **kwargs
|
||||
):
|
||||
SaleOrder = request.env['sale.order']
|
||||
|
||||
if not sortby:
|
||||
sortby = 'date'
|
||||
|
||||
partner = request.env.user.partner_id
|
||||
values = self._prepare_portal_layout_values()
|
||||
|
||||
if quotation_page:
|
||||
url = "/my/quotes"
|
||||
domain = self._prepare_quotations_domain(partner)
|
||||
else:
|
||||
url = "/my/orders"
|
||||
domain = self._prepare_orders_domain(partner)
|
||||
|
||||
searchbar_sortings = self._get_sale_searchbar_sortings()
|
||||
|
||||
sort_order = searchbar_sortings[sortby]['order']
|
||||
|
||||
if date_begin and date_end:
|
||||
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||
|
||||
pager_values = portal_pager(
|
||||
url=url,
|
||||
total=SaleOrder.search_count(domain),
|
||||
page=page,
|
||||
step=self._items_per_page,
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
|
||||
)
|
||||
orders = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager_values['offset'])
|
||||
|
||||
values.update({
|
||||
'date': date_begin,
|
||||
'quotations': orders.sudo() if quotation_page else SaleOrder,
|
||||
'orders': orders.sudo() if not quotation_page else SaleOrder,
|
||||
'page_name': 'quote' if quotation_page else 'order',
|
||||
'pager': pager_values,
|
||||
'default_url': url,
|
||||
'searchbar_sortings': searchbar_sortings,
|
||||
'sortby': sortby,
|
||||
})
|
||||
|
||||
return values
|
||||
|
||||
@http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def portal_my_quotes(self, **kwargs):
|
||||
values = self._prepare_sale_portal_rendering_values(quotation_page=True, **kwargs)
|
||||
request.session['my_quotations_history'] = values['quotations'].ids[:100]
|
||||
return request.render("sale.portal_my_quotations", values)
|
||||
|
||||
@http.route(['/my/orders', '/my/orders/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def portal_my_orders(self, **kwargs):
|
||||
values = self._prepare_sale_portal_rendering_values(quotation_page=False, **kwargs)
|
||||
request.session['my_orders_history'] = values['orders'].ids[:100]
|
||||
return request.render("sale.portal_my_orders", values)
|
||||
|
||||
@http.route(['/my/orders/<int:order_id>'], type='http', auth="public", website=True)
|
||||
def portal_order_page(
|
||||
self,
|
||||
order_id,
|
||||
report_type=None,
|
||||
access_token=None,
|
||||
message=False,
|
||||
download=False,
|
||||
downpayment=None,
|
||||
**kw
|
||||
):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
if report_type in ('html', 'pdf', 'text'):
|
||||
return self._show_report(
|
||||
model=order_sudo,
|
||||
report_type=report_type,
|
||||
report_ref='sale.action_report_saleorder',
|
||||
download=download,
|
||||
)
|
||||
|
||||
if request.env.user.share and access_token:
|
||||
# If a public/portal user accesses the order with the access token
|
||||
# Log a note on the chatter.
|
||||
today = fields.Date.today().isoformat()
|
||||
session_obj_date = request.session.get('view_quote_%s' % order_sudo.id)
|
||||
if session_obj_date != today:
|
||||
# store the date as a string in the session to allow serialization
|
||||
request.session['view_quote_%s' % order_sudo.id] = today
|
||||
# The "Quotation viewed by customer" log note is an information
|
||||
# dedicated to the salesman and shouldn't be translated in the customer/website lgg
|
||||
context = {'lang': order_sudo.user_id.partner_id.lang or order_sudo.company_id.partner_id.lang}
|
||||
msg = _('Quotation viewed by customer %s', order_sudo.partner_id.name if request.env.user._is_public() else request.env.user.partner_id.name)
|
||||
del context
|
||||
_message_post_helper(
|
||||
"sale.order",
|
||||
order_sudo.id,
|
||||
message=msg,
|
||||
token=order_sudo.access_token,
|
||||
message_type="notification",
|
||||
subtype_xmlid="mail.mt_note",
|
||||
partner_ids=order_sudo.user_id.sudo().partner_id.ids,
|
||||
)
|
||||
|
||||
backend_url = f'/web#model={order_sudo._name}'\
|
||||
f'&id={order_sudo.id}'\
|
||||
f'&action={order_sudo._get_portal_return_action().id}'\
|
||||
f'&view_type=form'
|
||||
values = {
|
||||
'sale_order': order_sudo,
|
||||
'product_documents': order_sudo._get_product_documents(),
|
||||
'message': message,
|
||||
'report_type': 'html',
|
||||
'backend_url': backend_url,
|
||||
'res_company': order_sudo.company_id, # Used to display correct company logo
|
||||
}
|
||||
|
||||
# Payment values
|
||||
if order_sudo._has_to_be_paid():
|
||||
values.update(
|
||||
self._get_payment_values(
|
||||
order_sudo,
|
||||
downpayment=downpayment == 'true' if downpayment is not None else order_sudo.prepayment_percent < 1.0
|
||||
)
|
||||
)
|
||||
|
||||
if order_sudo.state in ('draft', 'sent', 'cancel'):
|
||||
history_session_key = 'my_quotations_history'
|
||||
else:
|
||||
history_session_key = 'my_orders_history'
|
||||
|
||||
values = self._get_page_view_values(
|
||||
order_sudo, access_token, values, history_session_key, False)
|
||||
|
||||
return request.render('sale.sale_order_portal_template', values)
|
||||
|
||||
def _get_payment_values(self, order_sudo, downpayment=False, **kwargs):
|
||||
""" Return the payment-specific QWeb context values.
|
||||
|
||||
:param sale.order order_sudo: The sales order being paid.
|
||||
:param bool downpayment: Whether the current payment is a downpayment.
|
||||
:param dict kwargs: Locally unused data passed to `_get_compatible_providers` and
|
||||
`_get_available_tokens`.
|
||||
:return: The payment-specific values.
|
||||
:rtype: dict
|
||||
"""
|
||||
logged_in = not request.env.user._is_public()
|
||||
partner_sudo = request.env.user.partner_id if logged_in else order_sudo.partner_id
|
||||
company = order_sudo.company_id
|
||||
if downpayment:
|
||||
amount = order_sudo._get_prepayment_required_amount()
|
||||
else:
|
||||
amount = order_sudo.amount_total - order_sudo.amount_paid
|
||||
currency = order_sudo.currency_id
|
||||
|
||||
# Select all the payment methods and tokens that match the payment context.
|
||||
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
|
||||
company.id,
|
||||
partner_sudo.id,
|
||||
amount,
|
||||
currency_id=currency.id,
|
||||
sale_order_id=order_sudo.id,
|
||||
**kwargs,
|
||||
) # In sudo mode to read the fields of providers and partner (if logged out).
|
||||
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
|
||||
providers_sudo.ids,
|
||||
partner_sudo.id,
|
||||
currency_id=currency.id,
|
||||
) # In sudo mode to read the fields of providers.
|
||||
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
|
||||
providers_sudo.ids, partner_sudo.id, **kwargs
|
||||
) # In sudo mode to read the partner's tokens (if logged out) and provider fields.
|
||||
|
||||
# Make sure that the partner's company matches the invoice's company.
|
||||
company_mismatch = not payment_portal.PaymentPortal._can_partner_pay_in_company(
|
||||
partner_sudo, company
|
||||
)
|
||||
|
||||
portal_page_values = {
|
||||
'company_mismatch': company_mismatch,
|
||||
'expected_company': company,
|
||||
}
|
||||
payment_form_values = {
|
||||
'show_tokenize_input_mapping': PaymentPortal._compute_show_tokenize_input_mapping(
|
||||
providers_sudo, sale_order_id=order_sudo.id
|
||||
),
|
||||
}
|
||||
payment_context = {
|
||||
'amount': amount,
|
||||
'currency': currency,
|
||||
'partner_id': partner_sudo.id,
|
||||
'providers_sudo': providers_sudo,
|
||||
'payment_methods_sudo': payment_methods_sudo,
|
||||
'tokens_sudo': tokens_sudo,
|
||||
'transaction_route': order_sudo.get_portal_url(suffix='/transaction'),
|
||||
'landing_route': order_sudo.get_portal_url(),
|
||||
'access_token': order_sudo._portal_ensure_token(),
|
||||
}
|
||||
return {
|
||||
**portal_page_values,
|
||||
**payment_form_values,
|
||||
**payment_context,
|
||||
**self._get_extra_payment_form_values(**kwargs),
|
||||
}
|
||||
|
||||
@http.route(['/my/orders/<int:order_id>/accept'], type='json', auth="public", website=True)
|
||||
def portal_quote_accept(self, order_id, access_token=None, name=None, signature=None):
|
||||
# get from query string if not on json param
|
||||
access_token = access_token or request.httprequest.args.get('access_token')
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except (AccessError, MissingError):
|
||||
return {'error': _('Invalid order.')}
|
||||
|
||||
if not order_sudo._has_to_be_signed():
|
||||
return {'error': _('The order is not in a state requiring customer signature.')}
|
||||
if not signature:
|
||||
return {'error': _('Signature is missing.')}
|
||||
|
||||
try:
|
||||
order_sudo.write({
|
||||
'signed_by': name,
|
||||
'signed_on': fields.Datetime.now(),
|
||||
'signature': signature,
|
||||
})
|
||||
request.env.cr.commit()
|
||||
except (TypeError, binascii.Error) as e:
|
||||
return {'error': _('Invalid signature data.')}
|
||||
|
||||
if not order_sudo._has_to_be_paid():
|
||||
order_sudo.action_confirm()
|
||||
order_sudo._send_order_confirmation_mail()
|
||||
|
||||
pdf = request.env['ir.actions.report'].sudo()._render_qweb_pdf('sale.action_report_saleorder', [order_sudo.id])[0]
|
||||
|
||||
_message_post_helper(
|
||||
'sale.order',
|
||||
order_sudo.id,
|
||||
_('Order signed by %s', name),
|
||||
attachments=[('%s.pdf' % order_sudo.name, pdf)],
|
||||
token=access_token,
|
||||
)
|
||||
|
||||
query_string = '&message=sign_ok'
|
||||
if order_sudo._has_to_be_paid():
|
||||
query_string += '#allow_payment=yes'
|
||||
return {
|
||||
'force_refresh': True,
|
||||
'redirect_url': order_sudo.get_portal_url(query_string=query_string),
|
||||
}
|
||||
|
||||
@http.route(['/my/orders/<int:order_id>/decline'], type='http', auth="public", methods=['POST'], website=True)
|
||||
def portal_quote_decline(self, order_id, access_token=None, decline_message=None, **kwargs):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
if order_sudo._has_to_be_signed() and decline_message:
|
||||
order_sudo._action_cancel()
|
||||
_message_post_helper(
|
||||
'sale.order',
|
||||
order_sudo.id,
|
||||
decline_message,
|
||||
token=access_token,
|
||||
)
|
||||
redirect_url = order_sudo.get_portal_url()
|
||||
else:
|
||||
redirect_url = order_sudo.get_portal_url(query_string="&message=cant_reject")
|
||||
|
||||
return request.redirect(redirect_url)
|
||||
|
||||
@http.route('/my/orders/<int:order_id>/document/<int:document_id>', type='http', auth='public')
|
||||
def portal_quote_document(self, order_id, document_id, access_token):
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token=access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
document = request.env['product.document'].browse(document_id).sudo().exists()
|
||||
if not document or not document.active:
|
||||
return request.redirect('/my')
|
||||
|
||||
if document not in order_sudo._get_product_documents():
|
||||
return request.redirect('/my')
|
||||
|
||||
return request.env['ir.binary']._get_stream_from(
|
||||
document.ir_attachment_id,
|
||||
).get_response(as_attachment=True)
|
||||
|
||||
|
||||
class PaymentPortal(payment_portal.PaymentPortal):
|
||||
|
||||
@http.route('/my/orders/<int:order_id>/transaction', type='json', auth='public')
|
||||
def portal_order_transaction(self, order_id, access_token, **kwargs):
|
||||
""" Create a draft transaction and return its processing values.
|
||||
|
||||
:param int order_id: The sales order to pay, as a `sale.order` id
|
||||
:param str access_token: The access token used to authenticate the request
|
||||
:param dict kwargs: Locally unused data passed to `_create_transaction`
|
||||
:return: The mandatory values for the processing of the transaction
|
||||
:rtype: dict
|
||||
:raise: ValidationError if the invoice id or the access token is invalid
|
||||
"""
|
||||
# Check the order id and the access token
|
||||
try:
|
||||
order_sudo = self._document_check_access('sale.order', order_id, access_token)
|
||||
except MissingError as error:
|
||||
raise error
|
||||
except AccessError:
|
||||
raise ValidationError(_("The access token is invalid."))
|
||||
|
||||
logged_in = not request.env.user._is_public()
|
||||
partner_sudo = request.env.user.partner_id if logged_in else order_sudo.partner_invoice_id
|
||||
self._validate_transaction_kwargs(kwargs)
|
||||
kwargs.update({
|
||||
'partner_id': partner_sudo.id,
|
||||
'currency_id': order_sudo.currency_id.id,
|
||||
'sale_order_id': order_id, # Include the SO to allow Subscriptions tokenizing the tx
|
||||
})
|
||||
tx_sudo = self._create_transaction(
|
||||
custom_create_values={'sale_order_ids': [Command.set([order_id])]}, **kwargs,
|
||||
)
|
||||
|
||||
return tx_sudo._get_processing_values()
|
||||
|
||||
# Payment overrides
|
||||
|
||||
@http.route()
|
||||
def payment_pay(self, *args, amount=None, sale_order_id=None, access_token=None, **kwargs):
|
||||
""" Override of `payment` to replace the missing transaction values by that of the sales
|
||||
order.
|
||||
|
||||
:param str amount: The (possibly partial) amount to pay used to check the access token
|
||||
:param str sale_order_id: The sale order for which a payment id made, as a `sale.order` id
|
||||
:param str access_token: The access token used to authenticate the partner
|
||||
:return: The result of the parent method
|
||||
:rtype: str
|
||||
:raise: ValidationError if the order id is invalid
|
||||
"""
|
||||
# Cast numeric parameters as int or float and void them if their str value is malformed
|
||||
amount = self._cast_as_float(amount)
|
||||
sale_order_id = self._cast_as_int(sale_order_id)
|
||||
if sale_order_id:
|
||||
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id).exists()
|
||||
if not order_sudo:
|
||||
raise ValidationError(_("The provided parameters are invalid."))
|
||||
|
||||
# Check the access token against the order values. Done after fetching the order as we
|
||||
# need the order fields to check the access token.
|
||||
if not payment_utils.check_access_token(
|
||||
access_token, order_sudo.partner_invoice_id.id, amount, order_sudo.currency_id.id
|
||||
):
|
||||
raise ValidationError(_("The provided parameters are invalid."))
|
||||
|
||||
kwargs.update({
|
||||
# To display on the payment form; will be later overwritten when creating the tx.
|
||||
'reference': order_sudo.name,
|
||||
# To fix the currency if incorrect and avoid mismatches when creating the tx.
|
||||
'currency_id': order_sudo.currency_id.id,
|
||||
# To fix the partner if incorrect and avoid mismatches when creating the tx.
|
||||
'partner_id': order_sudo.partner_invoice_id.id,
|
||||
'company_id': order_sudo.company_id.id,
|
||||
'sale_order_id': sale_order_id,
|
||||
})
|
||||
return super().payment_pay(*args, amount=amount, access_token=access_token, **kwargs)
|
||||
|
||||
def _get_extra_payment_form_values(self, sale_order_id=None, access_token=None, **kwargs):
|
||||
""" Override of `payment` to reroute the payment flow to the portal view of the sales order.
|
||||
|
||||
:param str sale_order_id: The sale order for which a payment is made, as a `sale.order` id.
|
||||
:param str access_token: The portal or payment access token, respectively if we are in a
|
||||
portal or payment link flow.
|
||||
:return: The extended rendering context values.
|
||||
:rtype: dict
|
||||
"""
|
||||
form_values = super()._get_extra_payment_form_values(
|
||||
sale_order_id=sale_order_id, access_token=access_token, **kwargs
|
||||
)
|
||||
if sale_order_id:
|
||||
sale_order_id = self._cast_as_int(sale_order_id)
|
||||
|
||||
try: # Check document access against what could be a portal access token.
|
||||
order_sudo = self._document_check_access('sale.order', sale_order_id, access_token)
|
||||
except AccessError: # It is a payment access token computed on the payment context.
|
||||
if not payment_utils.check_access_token(
|
||||
access_token,
|
||||
kwargs.get('partner_id'),
|
||||
kwargs.get('amount'),
|
||||
kwargs.get('currency_id'),
|
||||
):
|
||||
raise
|
||||
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id)
|
||||
|
||||
# Interrupt the payment flow if the sales order has been canceled.
|
||||
if order_sudo.state == 'cancel':
|
||||
form_values['amount'] = 0.0
|
||||
|
||||
# Reroute the next steps of the payment flow to the portal view of the sales order.
|
||||
form_values.update({
|
||||
'transaction_route': order_sudo.get_portal_url(suffix='/transaction'),
|
||||
'landing_route': order_sudo.get_portal_url(),
|
||||
'access_token': order_sudo.access_token,
|
||||
})
|
||||
return form_values
|
14
data/ir_config_parameter.xml
Normal file
14
data/ir_config_parameter.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="default_confirmation_template" model="ir.config_parameter">
|
||||
<field name="key">sale.default_confirmation_template</field>
|
||||
<field name="value" ref="sale.mail_template_sale_confirmation"/>
|
||||
</record>
|
||||
|
||||
<record id="default_invoice_email_template" model="ir.config_parameter">
|
||||
<field name="key">sale.default_invoice_email_template</field>
|
||||
<field name="value" ref="account.email_template_edi_invoice"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
16
data/ir_cron.xml
Normal file
16
data/ir_cron.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="send_invoice_cron" model="ir.cron">
|
||||
<field name="name">automatic invoicing: send ready invoice</field>
|
||||
<field name="model_id" ref="payment.model_payment_transaction"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_send_invoice()</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="active">False</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
12
data/ir_sequence_data.xml
Normal file
12
data/ir_sequence_data.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="seq_sale_order" model="ir.sequence">
|
||||
<field name="name">Sales Order</field>
|
||||
<field name="code">sale.order</field>
|
||||
<field name="prefix">S</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
11
data/mail_activity_type_data.xml
Normal file
11
data/mail_activity_type_data.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Activities -->
|
||||
<record id="mail_act_sale_upsell" model="mail.activity.type">
|
||||
<field name="name">Order Upsell</field>
|
||||
<field name="icon">fa-line-chart</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
50
data/mail_message_subtype_data.xml
Normal file
50
data/mail_message_subtype_data.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Sale-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_order_sent" model="mail.message.subtype">
|
||||
<field name="name">Quotation sent</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Quotation sent</field>
|
||||
</record>
|
||||
<record id="mt_order_confirmed" model="mail.message.subtype">
|
||||
<field name="name">Sales Order Confirmed</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Quotation confirmed</field>
|
||||
</record>
|
||||
|
||||
<!-- Salesteam-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_salesteam_order_sent" model="mail.message.subtype">
|
||||
<field name="name">Quotation sent</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="res_model">crm.team</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="parent_id" ref="sale.mt_order_sent"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
<record id="mt_salesteam_order_confirmed" model="mail.message.subtype">
|
||||
<field name="name">Sales Order Confirmed</field>
|
||||
<field name="sequence">21</field>
|
||||
<field name="res_model">crm.team</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="parent_id" ref="sale.mt_order_confirmed"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
<record id="mt_salesteam_invoice_created" model="mail.message.subtype">
|
||||
<field name="name">Invoice Created</field>
|
||||
<field name="sequence">22</field>
|
||||
<field name="res_model">crm.team</field>
|
||||
<field name="parent_id" ref="account.mt_invoice_created"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
<record id="mt_salesteam_invoice_confirmed" model="mail.message.subtype">
|
||||
<field name="name">Invoice Confirmed</field>
|
||||
<field name="sequence">23</field>
|
||||
<field name="res_model">crm.team</field>
|
||||
<field name="parent_id" ref="account.mt_invoice_validated"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
330
data/mail_template_data.xml
Normal file
330
data/mail_template_data.xml
Normal file
@ -0,0 +1,330 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="email_template_edi_sale" model="mail.template">
|
||||
<field name="name">Sales: Send Quotation</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">{{ object.company_id.name }} {{ object.state in ('draft', 'sent') and (ctx.get('proforma') and 'Proforma' or 'Quotation') or 'Order' }} (Ref {{ object.name or 'n/a' }})</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="description">Used by salespeople when they send quotations or proforma to prospects</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
<t t-set="doc_name" t-value="'quotation' if object.state in ('draft', 'sent') else 'order'"/>
|
||||
Hello,
|
||||
<br/><br/>
|
||||
Your
|
||||
<t t-if="ctx.get('proforma')">
|
||||
Pro forma invoice for <t t-out="doc_name or ''">quotation</t> <span style="font-weight: bold;" t-out="object.name or ''">S00052</span>
|
||||
<t t-if="object.origin">
|
||||
(with reference: <t t-out="object.origin or ''"></t> )
|
||||
</t>
|
||||
amounting in <span style="font-weight: bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span> is available.
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-out="doc_name or ''">quotation</t> <span style="font-weight: bold;" t-out="object.name or ''"></span>
|
||||
<t t-if="object.origin">
|
||||
(with reference: <t t-out="object.origin or ''">S00052</t> )
|
||||
</t>
|
||||
amounting in <span style="font-weight: bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span> is ready for review.
|
||||
</t>
|
||||
<br/><br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br/><br/>
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_template_sale_confirmation" model="mail.template">
|
||||
<field name="name">Sales: Order Confirmation</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">{{ object.company_id.name }} {{ (object.get_portal_last_transaction().state == 'pending') and 'Pending Order' or 'Order' }} (Ref {{ object.name or 'n/a' }})</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="description">Sent to customers on order confirmation</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 12px;">
|
||||
Hello,
|
||||
<br/><br/>
|
||||
<t t-set="tx_sudo" t-value="object.get_portal_last_transaction()"/>
|
||||
Your order <span style="font-weight:bold;" t-out="object.name or ''">S00049</span> amounting in <span style="font-weight:bold;" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</span>
|
||||
<t t-if="object.state == 'sale' or (tx_sudo and tx_sudo.state in ('done', 'authorized'))">
|
||||
has been confirmed.<br/>
|
||||
Thank you for your trust!
|
||||
</t>
|
||||
<t t-elif="tx_sudo and tx_sudo.state == 'pending'">
|
||||
is pending. It will be confirmed when the payment is received.
|
||||
<t t-if="object.reference">
|
||||
Your payment reference is <span style="font-weight:bold;" t-out="object.reference or ''"></span>.
|
||||
</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br/><br/>
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
<t t-if="hasattr(object, 'website_id') and object.website_id">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
|
||||
<tr style="border-bottom: 2px solid #dee2e6;">
|
||||
<td style="width: 150px;"><span style="font-weight:bold;">Products</span></td>
|
||||
<td></td>
|
||||
<td width="15%" align="center"><span style="font-weight:bold;">Quantity</span></td>
|
||||
<td width="20%" align="right">
|
||||
<span style="font-weight:bold;">
|
||||
<t t-if="object.website_id.show_line_subtotals_tax_selection == 'tax_excluded'">
|
||||
VAT Excl.
|
||||
</t>
|
||||
<t t-else="">
|
||||
VAT Incl.
|
||||
</t>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<t t-foreach="object.order_line" t-as="line">
|
||||
<t t-if="(not hasattr(line, 'is_delivery') or not line.is_delivery) and line.display_type in ['line_section', 'line_note']">
|
||||
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
|
||||
<t t-set="loop_cycle_number" t-value="loop_cycle_number or 0" />
|
||||
<tr t-att-style="'background-color: #f2f2f2' if loop_cycle_number % 2 == 0 else 'background-color: #ffffff'">
|
||||
<t t-set="loop_cycle_number" t-value="loop_cycle_number + 1" />
|
||||
<td colspan="4">
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<span style="font-weight:bold;" t-out="line.name or ''">Taking care of Trees Course</span>
|
||||
</t>
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<i t-out="line.name or ''">Taking care of Trees Course</i>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
<t t-elif="(not hasattr(line, 'is_delivery') or not line.is_delivery)">
|
||||
<table width="100%" style="color: #454748; font-size: 12px; border-collapse: collapse;">
|
||||
<t t-set="loop_cycle_number" t-value="loop_cycle_number or 0" />
|
||||
<tr t-att-style="'background-color: #f2f2f2' if loop_cycle_number % 2 == 0 else 'background-color: #ffffff'">
|
||||
<t t-set="loop_cycle_number" t-value="loop_cycle_number + 1" />
|
||||
<td style="width: 150px;">
|
||||
<img t-attf-src="/web/image/product.product/{{ line.product_id.id }}/image_128" style="width: 64px; height: 64px; object-fit: contain;" alt="Product image"></img>
|
||||
</td>
|
||||
<td align="left" t-out="line.product_id.name or ''"> Taking care of Trees Course</td>
|
||||
<td width="15%" align="center" t-out="line.product_uom_qty or ''">1</td>
|
||||
<td width="20%" align="right"><span style="font-weight:bold;">
|
||||
<t t-if="object.website_id.show_line_subtotals_tax_selection == 'tax_excluded'">
|
||||
<t t-out="format_amount(line.price_reduce_taxexcl, object.currency_id) or ''">$ 10.00</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-out="format_amount(line.price_reduce_taxinc, object.currency_id) or ''">$ 10.00</t>
|
||||
</t>
|
||||
</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div style="margin: 0px; padding: 0px;" t-if="hasattr(object, 'carrier_id') and object.carrier_id">
|
||||
<table width="100%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
|
||||
<tr>
|
||||
<td style="width: 60%"/>
|
||||
<td style="width: 30%; border-top: 1px solid #dee2e6;" align="right"><span style="font-weight:bold;">Delivery:</span></td>
|
||||
<td style="width: 10%; border-top: 1px solid #dee2e6;" align="right" t-out="format_amount(object.amount_delivery, object.currency_id) or ''">$ 0.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 60%"/>
|
||||
<td style="width: 30%;" align="right"><span style="font-weight:bold;">SubTotal:</span></td>
|
||||
<td style="width: 10%;" align="right" t-out="format_amount(object.amount_untaxed, object.currency_id) or ''">$ 10.00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin: 0px; padding: 0px;" t-else="">
|
||||
<table width="100%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
|
||||
<tr>
|
||||
<td style="width: 60%"/>
|
||||
<td style="width: 30%; border-top: 1px solid #dee2e6;" align="right"><span style="font-weight:bold;">SubTotal:</span></td>
|
||||
<td style="width: 10%; border-top: 1px solid #dee2e6;" align="right" t-out="format_amount(object.amount_untaxed, object.currency_id) or ''">$ 10.00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<table width="100%" style="color: #454748; font-size: 12px; border-spacing: 0px 4px;" align="right">
|
||||
<tr>
|
||||
<td style="width: 60%"/>
|
||||
<td style="width: 30%;" align="right"><span style="font-weight:bold;">Taxes:</span></td>
|
||||
<td style="width: 10%;" align="right" t-out="format_amount(object.amount_tax, object.currency_id) or ''">$ 0.00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 60%"/>
|
||||
<td style="width: 30%; border-top: 1px solid #dee2e6;" align="right"><span style="font-weight:bold;">Total:</span></td>
|
||||
<td style="width: 10%; border-top: 1px solid #dee2e6;" align="right" t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 10.00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div t-if="object.partner_invoice_id" style="margin: 0px; padding: 0px;">
|
||||
<table width="100%" style="color: #454748; font-size: 12px;">
|
||||
<tr>
|
||||
<td style="padding-top: 10px;">
|
||||
<span style="font-weight:bold;">Bill to:</span>
|
||||
<t t-out="object.partner_invoice_id.street or ''">1201 S Figueroa St</t>
|
||||
<t t-out="object.partner_invoice_id.city or ''">Los Angeles</t>
|
||||
<t t-out="object.partner_invoice_id.state_id.name or ''">California</t>
|
||||
<t t-out="object.partner_invoice_id.zip or ''">90015</t>
|
||||
<t t-out="object.partner_invoice_id.country_id.name or ''">United States</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span style="font-weight:bold;">Payment Method:</span>
|
||||
<t t-if="tx_sudo.token_id">
|
||||
<t t-out="tx_sudo.token_id.display_name or ''"></t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-out="tx_sudo.provider_id.sudo().name or ''"></t>
|
||||
</t>
|
||||
(<t t-out="format_amount(tx_sudo.amount, object.currency_id) or ''">$ 10.00</t>)
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div t-if="object.partner_shipping_id and not object.only_services" style="margin: 0px; padding: 0px;">
|
||||
<table width="100%" style="color: #454748; font-size: 12px;">
|
||||
<tr>
|
||||
<td>
|
||||
<br/>
|
||||
<span style="font-weight:bold;">Ship to:</span>
|
||||
<t t-out="object.partner_shipping_id.street or ''">1201 S Figueroa St</t>
|
||||
<t t-out="object.partner_shipping_id.city or ''">Los Angeles</t>
|
||||
<t t-out="object.partner_shipping_id.state_id.name or ''">California</t>
|
||||
<t t-out="object.partner_shipping_id.zip or ''">90015</t>
|
||||
<t t-out="object.partner_shipping_id.country_id.name or ''">United States</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table t-if="hasattr(object, 'carrier_id') and object.carrier_id" width="100%" style="color: #454748; font-size: 12px;">
|
||||
<tr>
|
||||
<td>
|
||||
<span style="font-weight:bold;">Shipping Method:</span>
|
||||
<t t-out="object.carrier_id.name or ''"></t>
|
||||
<t t-if="object.amount_delivery == 0.0">
|
||||
(Free)
|
||||
</t>
|
||||
<t t-else="">
|
||||
(<t t-out="format_amount(object.amount_delivery, object.currency_id) or ''">$ 10.00</t>)
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="object.carrier_id.carrier_description">
|
||||
<td>
|
||||
<strong>Shipping Description:</strong>
|
||||
<t t-out="object.carrier_id.carrier_description"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div></field>
|
||||
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_template_sale_payment_executed" model="mail.template">
|
||||
<field name="name">Sales: Payment Done</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">{{ object.company_id.name }} {{ (object.get_portal_last_transaction().state == 'pending') and 'Pending Order' or 'Order' }} (Ref {{ object.name or 'n/a' }})</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="description">Sent to customers when a payment is received but doesn't immediately confirm their order</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 12px;">
|
||||
<t t-set="transaction_sudo" t-value="object.get_portal_last_transaction()"/>
|
||||
Hello,
|
||||
<br/><br/>
|
||||
A payment with reference
|
||||
<span style="font-weight:bold;" t-out="transaction_sudo.reference or ''">SOOO49</span>
|
||||
amounting
|
||||
<span style="font-weight:bold;" t-out="format_amount(transaction_sudo.amount, object.currency_id) or ''">$ 10.00</span>
|
||||
for your order
|
||||
<span style="font-weight:bold;" t-out="object.name or ''">S00049</span>
|
||||
<t t-if="transaction_sudo and transaction_sudo.state == 'pending'">
|
||||
is pending.
|
||||
<br/>
|
||||
<t t-if="object.currency_id.compare_amounts(object.amount_paid + transaction_sudo.amount, object.amount_total) >= 0 and object.state in ('draft', 'sent')">
|
||||
Your order will be confirmed once the payment is confirmed.
|
||||
</t>
|
||||
<t t-else="">
|
||||
Once confirmed,
|
||||
<span style="font-weight:bold;" t-out="format_amount(object.amount_total - object.amount_paid - transaction_sudo.amount, object.currency_id) or ''">$ 10.00</span>
|
||||
will remain to be paid.
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
has been confirmed.
|
||||
<t t-if="object.currency_id.compare_amounts(object.amount_paid, object.amount_total) < 0">
|
||||
<br/>
|
||||
<span style="font-weight:bold;" t-out="format_amount(object.amount_total - object.amount_paid, object.currency_id) or ''">$ 10.00</span>
|
||||
remains to be paid.
|
||||
</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
Thank you for your trust!
|
||||
<br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br/><br/>
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template_ids" eval="[(4, ref('sale.action_report_saleorder'))]"/>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="sale.mail_template_sale_cancellation" model="mail.template">
|
||||
<field name="name">Sales: Order Cancellation</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">{{ object.company_id.name }} {{ object.type_name }} Cancelled (Ref {{ object.name or 'n/a' }})</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="description">Sent automatically to customers when you cancel an order</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
<t t-set="doc_name" t-value="object.type_name"/>
|
||||
Dear <t t-out="object.partner_id.name or ''">user</t>,
|
||||
<br/><br/>
|
||||
Please be advised that your
|
||||
<t t-out="doc_name or ''">quotation</t> <strong t-out="object.name or ''">S00052</strong>
|
||||
<t t-if="object.origin">
|
||||
(with reference: <t t-out="object.origin or ''">S00052</t> )
|
||||
</t>
|
||||
has been cancelled. Therefore, you should not be charged further for this order.
|
||||
If any refund is necessary, this will be executed at best convenience.
|
||||
<br/><br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<br/>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
41
data/onboarding_data.xml
Normal file
41
data/onboarding_data.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Sale Quotation - ONBOARDING STEPS -->
|
||||
<record id="onboarding_onboarding_step_sale_order_confirmation" model="onboarding.onboarding.step">
|
||||
<field name="title">Order Confirmation</field>
|
||||
<field name="description">Choose between electronic signatures or online payments.</field>
|
||||
<field name="button_text">Set payments</field>
|
||||
<field name="panel_step_open_action_name">action_open_step_sale_order_confirmation</field>
|
||||
<field name="step_image" type="base64" file="base/static/img/onboarding_default.png"></field>
|
||||
<field name="step_image_filename">onboarding_default.png</field>
|
||||
<field name="step_image_alt">Onboarding Order Confirmation</field>
|
||||
<field name="sequence">6</field>
|
||||
</record>
|
||||
|
||||
<record id="onboarding_onboarding_step_sample_quotation" model="onboarding.onboarding.step">
|
||||
<field name="title">Sample Quotation</field>
|
||||
<field name="description">Send a quotation to test the customer portal.</field>
|
||||
<field name="button_text">Send sample</field>
|
||||
<field name="panel_step_open_action_name">action_open_step_sample_quotation</field>
|
||||
<field name="step_image" type="base64" file="base/static/img/onboarding_sample-quotation.png"></field>
|
||||
<field name="step_image_filename">onboarding_sample-quotation.png</field>
|
||||
<field name="step_image_alt">Onboarding Sample Quotation</field>
|
||||
<field name="sequence">7</field>
|
||||
</record>
|
||||
|
||||
<!-- Sale Quotation - ONBOARDING PANEL -->
|
||||
<record id="onboarding_onboarding_sale_quotation" model="onboarding.onboarding">
|
||||
<field name="name">Sale Quotation Onboarding</field>
|
||||
<field name="step_ids" eval="[
|
||||
Command.link(ref('sale.onboarding_onboarding_step_sale_order_confirmation')),
|
||||
Command.link(ref('sale.onboarding_onboarding_step_sample_quotation')),
|
||||
Command.link(ref('account.onboarding_onboarding_step_company_data')),
|
||||
Command.link(ref('account.onboarding_onboarding_step_base_document_layout'))
|
||||
]"/>
|
||||
<field name="route_name">sale_quotation</field>
|
||||
<field name="panel_close_action_name">action_close_panel_sale_quotation</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
201
data/product_demo.xml
Normal file
201
data/product_demo.xml
Normal file
@ -0,0 +1,201 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="product.consu_delivery_01" model="product.product">
|
||||
<field name="invoice_policy">order</field>
|
||||
</record>
|
||||
|
||||
<record id="product.consu_delivery_02" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.consu_delivery_03" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_order_01" model="product.product">
|
||||
<field name="invoice_policy">order</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_delivery_01" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_delivery_02" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_27" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_25" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_24" model="product.product">
|
||||
<field name="invoice_policy">order</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_22" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_20" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_16" model="product.product">
|
||||
<field name="invoice_policy">order</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_13" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_12" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_11b" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_11" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_10" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_9" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_8" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_7" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_6" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_5" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_4c" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_4b" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_4" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_3" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
<field name="expense_policy">cost</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_2" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_1" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
</record>
|
||||
|
||||
<!-- Expensable products -->
|
||||
<record id="product.expense_product" model="product.product">
|
||||
<field name="invoice_policy">order</field>
|
||||
<field name="expense_policy">sales_price</field>
|
||||
</record>
|
||||
|
||||
<record id="product.expense_hotel" model="product.product">
|
||||
<field name="invoice_policy">delivery</field>
|
||||
<field name="expense_policy">cost</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_attribute_2" model="product.attribute">
|
||||
<field name="display_type">color</field>
|
||||
</record>
|
||||
<record id="product.product_attribute_3" model="product.attribute">
|
||||
<field name="display_type">select</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_attribute_value_3" model="product.attribute.value">
|
||||
<field name="html_color">#FFFFFF</field>
|
||||
</record>
|
||||
<record id="product.product_attribute_value_4" model="product.attribute.value">
|
||||
<field name="html_color">#000000</field>
|
||||
</record>
|
||||
|
||||
<record id="product_attribute_value_7" model="product.attribute.value">
|
||||
<field name="name">Custom</field>
|
||||
<field name="attribute_id" ref="product.product_attribute_1"/>
|
||||
<field name="is_custom">True</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_4_attribute_1_product_template_attribute_line" model="product.template.attribute.line">
|
||||
<field name="value_ids" eval="[(4,ref('product_attribute_value_7'))]"/>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Handle automatically created product.template.attribute.value.
|
||||
Check "product.product_4_attribute_1_value_2" for more information about this
|
||||
-->
|
||||
<function model="ir.model.data" name="_update_xmlids">
|
||||
<value model="base" eval="[{
|
||||
'xml_id': 'sale.product_4_attribute_1_value_3',
|
||||
'record': obj().env.ref('product.product_4_attribute_1_product_template_attribute_line').product_template_value_ids[2],
|
||||
'noupdate': True,
|
||||
}]"/>
|
||||
</function>
|
||||
|
||||
<function model="ir.model.data" name="_update_xmlids">
|
||||
<value model="base" eval="[{
|
||||
'xml_id': 'sale.product_product_4e',
|
||||
'record': obj().env.ref('product.product_product_4_product_template')._get_variant_for_combination(obj().env.ref('sale.product_4_attribute_1_value_3') + obj().env.ref('product.product_4_attribute_2_value_1')),
|
||||
'noupdate': True,
|
||||
}, {
|
||||
'xml_id': 'sale.product_product_4f',
|
||||
'record': obj().env.ref('product.product_product_4_product_template')._get_variant_for_combination(obj().env.ref('sale.product_4_attribute_1_value_3') + obj().env.ref('product.product_4_attribute_2_value_2')),
|
||||
'noupdate': True,
|
||||
},]"/>
|
||||
</function>
|
||||
|
||||
<record id="product_product_4e" model="product.product">
|
||||
<field name="default_code">DESK0005</field>
|
||||
<field name="weight">0.01</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_4f" model="product.product">
|
||||
<field name="default_code">DESK0006</field>
|
||||
<field name="weight">0.01</field>
|
||||
</record>
|
||||
|
||||
<record id="advance_product_0" model="product.product">
|
||||
<field name="name">Deposit</field>
|
||||
<field name="categ_id" ref="product.product_category_3"/>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">150.0</field>
|
||||
<field name="invoice_policy">order</field>
|
||||
<field name="standard_price">100.0</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit"/>
|
||||
<field name="company_id" eval="[]"/>
|
||||
<field name="image_1920" type="base64" file="sale/static/img/advance_product_0-image.jpg"/>
|
||||
<field name="taxes_id" eval="[]"/>
|
||||
<field name="supplier_taxes_id" eval="[]"/>
|
||||
</record>
|
||||
</odoo>
|
712
data/sale_demo.xml
Normal file
712
data/sale_demo.xml
Normal file
@ -0,0 +1,712 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- We want to activate pay and sign by default for easier demoing. -->
|
||||
<record id="base.main_company" model="res.company">
|
||||
<field name="portal_confirmation_pay" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field eval="[(4, ref('sales_team.group_sale_salesman'))]" name="groups_id"/>
|
||||
</record>
|
||||
|
||||
<record model="crm.team" id="sales_team.team_sales_department">
|
||||
<field name="invoiced_target">250000</field>
|
||||
</record>
|
||||
|
||||
<record model="crm.team" id="sales_team.crm_team_1">
|
||||
<field name="invoiced_target">40000</field>
|
||||
</record>
|
||||
|
||||
<record id="utm_source_sale_order_0" model="utm.source">
|
||||
<field name="name">Sale Promotion 1</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_1" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_2"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_1" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_1"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_2" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_1"/>
|
||||
<field name="product_id" ref="product.product_delivery_02"/>
|
||||
<field name="product_uom_qty">5</field>
|
||||
<field name="price_unit">145.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_3" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_1"/>
|
||||
<field name="product_id" ref="product.product_delivery_01"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">65.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_2" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_13"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_13"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor7'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_4" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_2"/>
|
||||
<field name="product_id" ref="product.product_product_1"/>
|
||||
<field name="product_uom_qty">24</field>
|
||||
<field name="price_unit">75.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_5" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_2"/>
|
||||
<field name="product_id" ref="product.product_product_2"/>
|
||||
<field name="product_uom_qty">30</field>
|
||||
<field name="price_unit">38.25</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_3" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_4"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_4"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor1')), (4, ref('sales_team.categ_oppor2'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_6" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_3"/>
|
||||
<field name="product_id" ref="product.product_product_1"/>
|
||||
<field name="product_uom_qty">10</field>
|
||||
<field name="price_unit">30.75</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_7" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_3"/>
|
||||
<field name="product_id" ref="product.product_delivery_01"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_4" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_8" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_4"/>
|
||||
<field name="product_id" ref="product.product_product_1"/>
|
||||
<field name="product_uom_qty">16</field>
|
||||
<field name="price_unit">75.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_9" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_4"/>
|
||||
<field name="product_id" ref="product.product_delivery_02"/>
|
||||
<field name="product_uom_qty">10</field>
|
||||
<field name="price_unit">45.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_10" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_4"/>
|
||||
<field name="product_id" ref="product.consu_delivery_02"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">150.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_11" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_4"/>
|
||||
<field name="product_id" ref="product.product_delivery_01"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_5" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_2"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_12" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_5"/>
|
||||
<field name="product_id" ref="product.product_delivery_02"/>
|
||||
<field name="price_unit">405.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_6" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_18"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_18"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_18"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor6'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_15" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_6"/>
|
||||
<field name="product_id" ref="product.product_product_4"/>
|
||||
<field name="price_unit">750.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_7" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_11"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_11"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor4'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_16" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_7"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">5</field>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_17" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_7"/>
|
||||
<field name="product_id" ref="product.consu_delivery_01"/>
|
||||
<field name="price_unit">173.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_18" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_7"/>
|
||||
<field name="product_id" ref="product.product_delivery_02"/>
|
||||
<field name="price_unit">40.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_19" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_7"/>
|
||||
<field name="product_id" ref="product.product_delivery_01"/>
|
||||
<field name="price_unit">18.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_8" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_20" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_8"/>
|
||||
<field name="product_id" ref="product.product_product_27"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">110.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_21" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_8"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<!-- additional demo data for pretty graphs in sales dashboard -->
|
||||
|
||||
<record id="sale_order_9" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_22" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_9"/>
|
||||
<field name="product_id" ref="product.product_product_27"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">97.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_23" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_9"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_10" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=14)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor3'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_24" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_10"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">255.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_25" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_10"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="sale_order_11" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=21)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_26" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_11"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">245.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_27" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_11"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_12" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=28)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor1'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_28" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_12"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="price_unit">315.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_29" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_12"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_13" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.crm_team_1"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=35)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_30" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_13"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_31" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_13"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_14" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_32" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_14"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">4</field>
|
||||
<field name="price_unit">275.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_33" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_14"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">4</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_15" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=14)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_34" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_15"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">4</field>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_35" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_15"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_16" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=21)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_36" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_16"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">275.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_37" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_16"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_17" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=28)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_38" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_17"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">355.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_39" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_17"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_18" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now()-relativedelta(days=35)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_40" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_18"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_41" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_18"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">120.50</field>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_1" model="sale.order">
|
||||
<field name="partner_id" ref="base.partner_demo_portal"/>
|
||||
<field name="partner_invoice_id" ref="base.partner_demo_portal"/>
|
||||
<field name="partner_shipping_id" ref="base.partner_demo_portal"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="state">sent</field>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="message_partner_ids" eval="[(4, ref('base.partner_demo_portal'))]"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor4'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_line_1" model="sale.order.line">
|
||||
<field name="order_id" ref="portal_sale_order_1"/>
|
||||
<field name="product_id" ref="product.product_product_25"/>
|
||||
<field name="product_uom_qty">3</field>
|
||||
<field name="price_unit">295.00</field>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_line_2" model="sale.order.line">
|
||||
<field name="order_id" ref="portal_sale_order_1"/>
|
||||
<field name="product_id" ref="product.product_delivery_02"/>
|
||||
<field name="product_uom_qty">5</field>
|
||||
<field name="price_unit">145.00</field>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_line_3" model="sale.order.line">
|
||||
<field name="order_id" ref="portal_sale_order_1"/>
|
||||
<field name="product_id" ref="product.product_delivery_01"/>
|
||||
<field name="product_uom_qty">2</field>
|
||||
<field name="price_unit">65.00</field>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_2" model="sale.order">
|
||||
<field name="partner_id" ref="base.partner_demo_portal"/>
|
||||
<field name="partner_invoice_id" ref="base.partner_demo_portal"/>
|
||||
<field name="partner_shipping_id" ref="base.partner_demo_portal"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="date_order" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="message_partner_ids" eval="[(4, ref('base.partner_demo_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_line_4" model="sale.order.line">
|
||||
<field name="order_id" ref="portal_sale_order_2"/>
|
||||
<field name="product_id" ref="product.product_product_1"/>
|
||||
<field name="product_uom_qty">24</field>
|
||||
<field name="price_unit">75.00</field>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_line_5" model="sale.order.line">
|
||||
<field name="order_id" ref="portal_sale_order_2"/>
|
||||
<field name="product_id" ref="product.product_product_2"/>
|
||||
<field name="product_uom_qty">30</field>
|
||||
<field name="price_unit">38.25</field>
|
||||
</record>
|
||||
|
||||
<!-- Confirm some Sales Orders-->
|
||||
<function model="sale.order" name="action_confirm" eval="[[
|
||||
ref('sale_order_4'),
|
||||
ref('sale_order_6'),
|
||||
ref('sale_order_7'),
|
||||
ref('sale_order_8'),
|
||||
ref('sale_order_9'),
|
||||
ref('sale_order_10'),
|
||||
ref('sale_order_11'),
|
||||
ref('sale_order_12'),
|
||||
ref('sale_order_13'),
|
||||
ref('sale_order_14'),
|
||||
ref('sale_order_15'),
|
||||
ref('sale_order_16'),
|
||||
ref('sale_order_17'),
|
||||
ref('sale_order_18'),
|
||||
ref('portal_sale_order_2'),
|
||||
]]"/>
|
||||
|
||||
<!-- Setting date_order in the past for beautiful spread -->
|
||||
<record id="sale_order_9" model="sale.order">
|
||||
<field name="date_order" eval="datetime.now() - relativedelta(days=7)"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_11" model="sale.order">
|
||||
<field name="date_order" eval="datetime.now() - relativedelta(days=21)"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_15" model="sale.order">
|
||||
<field name="date_order" eval="datetime.now() - relativedelta(days=14)"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_17" model="sale.order">
|
||||
<field name="date_order" eval="datetime.now() - relativedelta(days=28)"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_18" model="sale.order">
|
||||
<field name="date_order" eval="datetime.now() - relativedelta(days=35)"/>
|
||||
</record>
|
||||
|
||||
<record id="portal_sale_order_2" model="sale.order">
|
||||
<field name="date_order" eval="datetime.now() - relativedelta(months=1)"/>
|
||||
</record>
|
||||
|
||||
<!-- Mail messages in SO's chatter -->
|
||||
<record id="message_sale_1" model="mail.message">
|
||||
<field name="model">sale.order</field>
|
||||
<field name="res_id" ref="sale_order_2"/>
|
||||
<field name="body">Hi,
|
||||
I have a question regarding services pricing: I heard of a possible discount for quantities exceeding 25 hours.
|
||||
Could you confirm, please?</field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="message_sale_2" model="mail.message">
|
||||
<field name="model">sale.order</field>
|
||||
<field name="res_id" ref="sale_order_2"/>
|
||||
<field name="parent_id" ref="message_sale_1"/>
|
||||
<field name="body">Hello,
|
||||
Unfortunately that was a temporary discount that is not available anymore.
|
||||
Do you still plan to confirm the order based on the quoted prices?
|
||||
Thanks!</field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_root"/>
|
||||
</record>
|
||||
|
||||
<record id="message_sale_3" model="mail.message">
|
||||
<field name="model">sale.order</field>
|
||||
<field name="res_id" ref="sale_order_2"/>
|
||||
<field name="parent_id" ref="message_sale_2"/>
|
||||
<field name="body">
|
||||
Alright, thanks for the clarification. I will confirm the order as soon as I get my manager's approval.
|
||||
</field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
</record>
|
||||
|
||||
<!-- Activities of sales order -->
|
||||
<record id="sale_activity_2" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_3"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
|
||||
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="summary">Answer questions</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
<record id="sale_activity_3" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_4"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="sale.mail_act_sale_upsell"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
<record id="sale_activity_4" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_5"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
|
||||
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="create_uid" ref="base.user_demo"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
<record id="sale_activity_6" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_7"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="summary">Check delivery requirements</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
<record id="sale_activity_7" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_10"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="summary">Confirm Delivery</field>
|
||||
<field name="create_uid" ref="base.user_demo"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
<record id="sale_activity_8" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_12"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_email"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="create_uid" ref="base.user_demo"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
<record id="sale_activity_9" model="mail.activity">
|
||||
<field name="res_id" ref="sale.sale_order_16"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="sale.mail_act_sale_upsell"/>
|
||||
<field name="date_deadline" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="create_uid" ref="base.user_demo"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
<record id="sale_activity_10" model="mail.activity">
|
||||
<field name="res_id" ref="sale.portal_sale_order_1"/>
|
||||
<field name="res_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M')"/>
|
||||
<field name="summary">Get quote confirmation</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
4588
i18n/af.po
Normal file
4588
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
4584
i18n/am.po
Normal file
4584
i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
5669
i18n/ar.po
Normal file
5669
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
4598
i18n/az.po
Normal file
4598
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
5239
i18n/bg.po
Normal file
5239
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
4589
i18n/bs.po
Normal file
4589
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
5387
i18n/ca.po
Normal file
5387
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
5700
i18n/cs.po
Normal file
5700
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
5686
i18n/da.po
Normal file
5686
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
5761
i18n/de.po
Normal file
5761
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
4605
i18n/el.po
Normal file
4605
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
4871
i18n/en_AU.po
Normal file
4871
i18n/en_AU.po
Normal file
File diff suppressed because it is too large
Load Diff
4587
i18n/en_GB.po
Normal file
4587
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
5741
i18n/es.po
Normal file
5741
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
5741
i18n/es_419.po
Normal file
5741
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/es_BO.po
Normal file
4586
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
4587
i18n/es_CL.po
Normal file
4587
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
i18n/es_CO.po
Normal file
4590
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/es_CR.po
Normal file
4586
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
i18n/es_DO.po
Normal file
4590
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
4587
i18n/es_EC.po
Normal file
4587
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
4588
i18n/es_PE.po
Normal file
4588
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/es_PY.po
Normal file
4586
i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/es_VE.po
Normal file
4586
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
5663
i18n/et.po
Normal file
5663
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/eu.po
Normal file
4586
i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
5314
i18n/fa.po
Normal file
5314
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
5396
i18n/fi.po
Normal file
5396
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
4587
i18n/fo.po
Normal file
4587
i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
5738
i18n/fr.po
Normal file
5738
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/fr_CA.po
Normal file
4586
i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/gl.po
Normal file
4586
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
4594
i18n/gu.po
Normal file
4594
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
5290
i18n/he.po
Normal file
5290
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
4640
i18n/hr.po
Normal file
4640
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
5298
i18n/hu.po
Normal file
5298
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
5713
i18n/id.po
Normal file
5713
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
4592
i18n/is.po
Normal file
4592
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
5729
i18n/it.po
Normal file
5729
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
5551
i18n/ja.po
Normal file
5551
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
i18n/ka.po
Normal file
4590
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
4586
i18n/kab.po
Normal file
4586
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
i18n/km.po
Normal file
4590
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
5575
i18n/ko.po
Normal file
5575
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
4588
i18n/lb.po
Normal file
4588
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
4587
i18n/lo.po
Normal file
4587
i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
5341
i18n/lt.po
Normal file
5341
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
5759
i18n/lv.po
Normal file
5759
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
4591
i18n/mk.po
Normal file
4591
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
4629
i18n/mn.po
Normal file
4629
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
4620
i18n/nb.po
Normal file
4620
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
5731
i18n/nl.po
Normal file
5731
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
5407
i18n/pl.po
Normal file
5407
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
5281
i18n/pt.po
Normal file
5281
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
5711
i18n/pt_BR.po
Normal file
5711
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
4638
i18n/ro.po
Normal file
4638
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
5754
i18n/ru.po
Normal file
5754
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
5164
i18n/sale.pot
Normal file
5164
i18n/sale.pot
Normal file
File diff suppressed because it is too large
Load Diff
5318
i18n/sk.po
Normal file
5318
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
5280
i18n/sl.po
Normal file
5280
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
4588
i18n/sq.po
Normal file
4588
i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
5489
i18n/sr.po
Normal file
5489
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
4590
i18n/sr@latin.po
Normal file
4590
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
5370
i18n/sv.po
Normal file
5370
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
5658
i18n/th.po
Normal file
5658
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
5391
i18n/tr.po
Normal file
5391
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
5380
i18n/uk.po
Normal file
5380
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
5702
i18n/vi.po
Normal file
5702
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
5545
i18n/zh_CN.po
Normal file
5545
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
5545
i18n/zh_TW.po
Normal file
5545
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
19
models/__init__.py
Normal file
19
models/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import analytic
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import crm_team
|
||||
from . import onboarding_onboarding
|
||||
from . import onboarding_onboarding_step
|
||||
from . import payment_provider
|
||||
from . import payment_transaction
|
||||
from . import product_document
|
||||
from . import product_product
|
||||
from . import product_template
|
||||
from . import res_company
|
||||
from . import res_partner
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import utm_campaign
|
168
models/account_move.py
Normal file
168
models/account_move.py
Normal file
@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_name = 'account.move'
|
||||
_inherit = ['account.move', 'utm.mixin']
|
||||
|
||||
@api.model
|
||||
def _get_invoice_default_sale_team(self):
|
||||
return self.env['crm.team']._get_default_team_id()
|
||||
|
||||
team_id = fields.Many2one(
|
||||
'crm.team', string='Sales Team', default=_get_invoice_default_sale_team,
|
||||
compute='_compute_team_id', store=True, readonly=False,
|
||||
ondelete="set null", tracking=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
|
||||
# UTMs - enforcing the fact that we want to 'set null' when relation is unlinked
|
||||
campaign_id = fields.Many2one(ondelete='set null')
|
||||
medium_id = fields.Many2one(ondelete='set null')
|
||||
source_id = fields.Many2one(ondelete='set null')
|
||||
sale_order_count = fields.Integer(compute="_compute_origin_so_count", string='Sale Order Count')
|
||||
|
||||
def unlink(self):
|
||||
downpayment_lines = self.mapped('line_ids.sale_line_ids').filtered(lambda line: line.is_downpayment and line.invoice_lines <= self.mapped('line_ids'))
|
||||
res = super(AccountMove, self).unlink()
|
||||
if downpayment_lines:
|
||||
downpayment_lines.unlink()
|
||||
return res
|
||||
|
||||
@api.depends('invoice_user_id')
|
||||
def _compute_team_id(self):
|
||||
for move in self:
|
||||
if not move.invoice_user_id.sale_team_id or not move.is_sale_document(include_receipts=True):
|
||||
continue
|
||||
move.team_id = self.env['crm.team']._get_default_team_id(
|
||||
user_id=move.invoice_user_id.id,
|
||||
domain=[('company_id', '=', move.company_id.id)])
|
||||
|
||||
@api.depends('line_ids.sale_line_ids')
|
||||
def _compute_origin_so_count(self):
|
||||
for move in self:
|
||||
move.sale_order_count = len(move.line_ids.sale_line_ids.order_id)
|
||||
|
||||
def _reverse_moves(self, default_values_list=None, cancel=False):
|
||||
# OVERRIDE
|
||||
if not default_values_list:
|
||||
default_values_list = [{} for move in self]
|
||||
for move, default_values in zip(self, default_values_list):
|
||||
default_values.update({
|
||||
'campaign_id': move.campaign_id.id,
|
||||
'medium_id': move.medium_id.id,
|
||||
'source_id': move.source_id.id,
|
||||
})
|
||||
return super()._reverse_moves(default_values_list=default_values_list, cancel=cancel)
|
||||
|
||||
def action_post(self):
|
||||
# inherit of the function from account.move to validate a new tax and the priceunit of a downpayment
|
||||
res = super(AccountMove, self).action_post()
|
||||
|
||||
# We cannot change lines content on locked SO, changes on invoices are not forwarded to the SO if the SO is locked
|
||||
downpayment_lines = self.line_ids.sale_line_ids.filtered(lambda l: l.is_downpayment and not l.display_type and not l.order_id.locked)
|
||||
other_so_lines = downpayment_lines.order_id.order_line - downpayment_lines
|
||||
real_invoices = set(other_so_lines.invoice_lines.move_id)
|
||||
for so_dpl in downpayment_lines:
|
||||
so_dpl.price_unit = sum(
|
||||
l.price_unit if l.move_id.move_type == 'out_invoice' else -l.price_unit
|
||||
for l in so_dpl.invoice_lines
|
||||
if l.move_id.state == 'posted' and l.move_id not in real_invoices # don't recompute with the final invoice
|
||||
)
|
||||
so_dpl.tax_id = so_dpl.invoice_lines.tax_ids
|
||||
|
||||
return res
|
||||
|
||||
def button_draft(self):
|
||||
res = super().button_draft()
|
||||
|
||||
self.line_ids.filtered('is_downpayment').sale_line_ids.filtered(
|
||||
lambda sol: not sol.display_type)._compute_name()
|
||||
|
||||
return res
|
||||
|
||||
def button_cancel(self):
|
||||
res = super().button_cancel()
|
||||
|
||||
self.line_ids.filtered('is_downpayment').sale_line_ids.filtered(
|
||||
lambda sol: not sol.display_type)._compute_name()
|
||||
|
||||
return res
|
||||
|
||||
def _post(self, soft=True):
|
||||
# OVERRIDE
|
||||
# Auto-reconcile the invoice with payments coming from transactions.
|
||||
# It's useful when you have a "paid" sale order (using a payment transaction) and you invoice it later.
|
||||
posted = super()._post(soft)
|
||||
|
||||
for invoice in posted.filtered(lambda move: move.is_invoice()):
|
||||
payments = invoice.mapped('transaction_ids.payment_id').filtered(lambda x: x.state == 'posted')
|
||||
move_lines = payments.line_ids.filtered(lambda line: line.account_type in ('asset_receivable', 'liability_payable') and not line.reconciled)
|
||||
for line in move_lines:
|
||||
invoice.js_assign_outstanding_line(line.id)
|
||||
return posted
|
||||
|
||||
def _invoice_paid_hook(self):
|
||||
# OVERRIDE
|
||||
res = super(AccountMove, self)._invoice_paid_hook()
|
||||
todo = set()
|
||||
for invoice in self.filtered(lambda move: move.is_invoice()):
|
||||
for line in invoice.invoice_line_ids:
|
||||
for sale_line in line.sale_line_ids:
|
||||
todo.add((sale_line.order_id, invoice.name))
|
||||
for (order, name) in todo:
|
||||
order.message_post(body=_("Invoice %s paid", name))
|
||||
return res
|
||||
|
||||
def _action_invoice_ready_to_be_sent(self):
|
||||
# OVERRIDE
|
||||
# Make sure the send invoice CRON is called when an invoice becomes ready to be sent by mail.
|
||||
res = super()._action_invoice_ready_to_be_sent()
|
||||
|
||||
send_invoice_cron = self.env.ref('sale.send_invoice_cron', raise_if_not_found=False)
|
||||
if send_invoice_cron:
|
||||
send_invoice_cron._trigger()
|
||||
|
||||
return res
|
||||
|
||||
def action_view_source_sale_orders(self):
|
||||
self.ensure_one()
|
||||
source_orders = self.line_ids.sale_line_ids.order_id
|
||||
result = self.env['ir.actions.act_window']._for_xml_id('sale.action_orders')
|
||||
if len(source_orders) > 1:
|
||||
result['domain'] = [('id', 'in', source_orders.ids)]
|
||||
elif len(source_orders) == 1:
|
||||
result['views'] = [(self.env.ref('sale.view_order_form', False).id, 'form')]
|
||||
result['res_id'] = source_orders.id
|
||||
else:
|
||||
result = {'type': 'ir.actions.act_window_close'}
|
||||
return result
|
||||
|
||||
def _is_downpayment(self):
|
||||
# OVERRIDE
|
||||
self.ensure_one()
|
||||
return self.line_ids.sale_line_ids and all(sale_line.is_downpayment for sale_line in self.line_ids.sale_line_ids) or False
|
||||
|
||||
@api.depends('line_ids.sale_line_ids.order_id', 'currency_id', 'tax_totals', 'date')
|
||||
def _compute_partner_credit(self):
|
||||
super()._compute_partner_credit()
|
||||
for move in self.filtered(lambda m: m.is_invoice(include_receipts=True)):
|
||||
sale_orders = move.line_ids.sale_line_ids.order_id
|
||||
amount_total_currency = move.currency_id._convert(
|
||||
move.tax_totals['amount_total'],
|
||||
move.company_currency_id,
|
||||
move.company_id,
|
||||
move.date
|
||||
)
|
||||
amount_to_invoice_currency = sum(
|
||||
sale_order.currency_id._convert(
|
||||
sale_order.amount_to_invoice,
|
||||
move.company_currency_id,
|
||||
move.company_id,
|
||||
move.date
|
||||
) for sale_order in sale_orders
|
||||
)
|
||||
move.partner_credit += max(amount_total_currency - amount_to_invoice_currency, 0.0)
|
229
models/account_move_line.py
Normal file
229
models/account_move_line.py
Normal file
@ -0,0 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
is_downpayment = fields.Boolean()
|
||||
sale_line_ids = fields.Many2many(
|
||||
'sale.order.line',
|
||||
'sale_order_line_invoice_rel',
|
||||
'invoice_line_id', 'order_line_id',
|
||||
string='Sales Order Lines', readonly=True, copy=False)
|
||||
|
||||
def _copy_data_extend_business_fields(self, values):
|
||||
# OVERRIDE to copy the 'sale_line_ids' field as well.
|
||||
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
|
||||
values['sale_line_ids'] = [(6, None, self.sale_line_ids.ids)]
|
||||
|
||||
def _prepare_analytic_lines(self):
|
||||
""" Note: This method is called only on the move.line that having an analytic distribution, and
|
||||
so that should create analytic entries.
|
||||
"""
|
||||
values_list = super(AccountMoveLine, self)._prepare_analytic_lines()
|
||||
|
||||
# filter the move lines that can be reinvoiced: a cost (negative amount) analytic line without SO line but with a product can be reinvoiced
|
||||
move_to_reinvoice = self.env['account.move.line']
|
||||
if len(values_list) > 0:
|
||||
for index, move_line in enumerate(self):
|
||||
values = values_list[index]
|
||||
if 'so_line' not in values:
|
||||
if move_line._sale_can_be_reinvoice():
|
||||
move_to_reinvoice |= move_line
|
||||
|
||||
# insert the sale line in the create values of the analytic entries
|
||||
if move_to_reinvoice.filtered(lambda aml: not aml.move_id.reversed_entry_id): # only if the move line is not a reversal one
|
||||
map_sale_line_per_move = move_to_reinvoice._sale_create_reinvoice_sale_line()
|
||||
for values in values_list:
|
||||
sale_line = map_sale_line_per_move.get(values.get('move_line_id'))
|
||||
if sale_line:
|
||||
values['so_line'] = sale_line.id
|
||||
|
||||
return values_list
|
||||
|
||||
def _sale_can_be_reinvoice(self):
|
||||
""" determine if the generated analytic line should be reinvoiced or not.
|
||||
For Vendor Bill flow, if the product has a 'erinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.sale_line_ids:
|
||||
return False
|
||||
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
return float_compare(self.credit or 0.0, self.debit or 0.0, precision_digits=uom_precision_digits) != 1 and self.product_id.expense_policy not in [False, 'no']
|
||||
|
||||
def _sale_create_reinvoice_sale_line(self):
|
||||
|
||||
sale_order_map = self._sale_determine_order()
|
||||
|
||||
sale_line_values_to_create = [] # the list of creation values of sale line to create.
|
||||
existing_sale_line_cache = {} # in the sales_price-delivery case, we can reuse the same sale line. This cache will avoid doing a search each time the case happen
|
||||
# `map_move_sale_line` is map where
|
||||
# - key is the move line identifier
|
||||
# - value is either a sale.order.line record (existing case), or an integer representing the index of the sale line to create in
|
||||
# the `sale_line_values_to_create` (not existing case, which will happen more often than the first one).
|
||||
map_move_sale_line = {}
|
||||
|
||||
for move_line in self:
|
||||
sale_order = sale_order_map.get(move_line.id)
|
||||
|
||||
# no reinvoice as no sales order was found
|
||||
if not sale_order:
|
||||
continue
|
||||
|
||||
# raise if the sale order is not currently open
|
||||
if sale_order.state in ('draft', 'sent'):
|
||||
raise UserError(_(
|
||||
"The Sales Order %(order)s linked to the Analytic Account %(account)s must be"
|
||||
" validated before registering expenses.",
|
||||
order=sale_order.name,
|
||||
account=sale_order.analytic_account_id.name,
|
||||
))
|
||||
elif sale_order.state == 'cancel':
|
||||
raise UserError(_(
|
||||
"The Sales Order %(order)s linked to the Analytic Account %(account)s is cancelled."
|
||||
" You cannot register an expense on a cancelled Sales Order.",
|
||||
order=sale_order.name,
|
||||
account=sale_order.analytic_account_id.name,
|
||||
))
|
||||
elif sale_order.locked:
|
||||
raise UserError(_(
|
||||
"The Sales Order %(order)s linked to the Analytic Account %(account)s is currently locked."
|
||||
" You cannot register an expense on a locked Sales Order."
|
||||
" Please create a new SO linked to this Analytic Account.",
|
||||
order=sale_order.name,
|
||||
account=sale_order.analytic_account_id.name,
|
||||
))
|
||||
|
||||
price = move_line._sale_get_invoice_price(sale_order)
|
||||
|
||||
# find the existing sale.line or keep its creation values to process this in batch
|
||||
sale_line = None
|
||||
if (
|
||||
move_line.product_id.expense_policy == 'sales_price'
|
||||
and move_line.product_id.invoice_policy == 'delivery'
|
||||
and not self.env.context.get('force_split_lines')
|
||||
):
|
||||
# for those case only, we can try to reuse one
|
||||
map_entry_key = (sale_order.id, move_line.product_id.id, price) # cache entry to limit the call to search
|
||||
sale_line = existing_sale_line_cache.get(map_entry_key)
|
||||
if sale_line: # already search, so reuse it. sale_line can be sale.order.line record or index of a "to create values" in `sale_line_values_to_create`
|
||||
map_move_sale_line[move_line.id] = sale_line
|
||||
existing_sale_line_cache[map_entry_key] = sale_line
|
||||
else: # search for existing sale line
|
||||
sale_line = self.env['sale.order.line'].search([
|
||||
('order_id', '=', sale_order.id),
|
||||
('price_unit', '=', price),
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('is_expense', '=', True),
|
||||
], limit=1)
|
||||
if sale_line: # found existing one, so keep the browse record
|
||||
map_move_sale_line[move_line.id] = existing_sale_line_cache[map_entry_key] = sale_line
|
||||
else: # should be create, so use the index of creation values instead of browse record
|
||||
# save value to create it
|
||||
sale_line_values_to_create.append(move_line._sale_prepare_sale_line_values(sale_order, price))
|
||||
# store it in the cache of existing ones
|
||||
existing_sale_line_cache[map_entry_key] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
|
||||
# store it in the map_move_sale_line map
|
||||
map_move_sale_line[move_line.id] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
|
||||
|
||||
else: # save its value to create it anyway
|
||||
sale_line_values_to_create.append(move_line._sale_prepare_sale_line_values(sale_order, price))
|
||||
map_move_sale_line[move_line.id] = len(sale_line_values_to_create) - 1 # save the index of the value to create sale line
|
||||
|
||||
# create the sale lines in batch
|
||||
new_sale_lines = self.env['sale.order.line'].create(sale_line_values_to_create)
|
||||
|
||||
# build result map by replacing index with newly created record of sale.order.line
|
||||
result = {}
|
||||
for move_line_id, unknown_sale_line in map_move_sale_line.items():
|
||||
if isinstance(unknown_sale_line, int): # index of newly created sale line
|
||||
result[move_line_id] = new_sale_lines[unknown_sale_line]
|
||||
elif isinstance(unknown_sale_line, models.BaseModel): # already record of sale.order.line
|
||||
result[move_line_id] = unknown_sale_line
|
||||
return result
|
||||
|
||||
def _sale_determine_order(self):
|
||||
""" Get the mapping of move.line with the sale.order record on which its analytic entries should be reinvoiced
|
||||
:return a dict where key is the move line id, and value is sale.order record (or None).
|
||||
"""
|
||||
mapping = {}
|
||||
for move_line in self:
|
||||
if move_line.analytic_distribution:
|
||||
distribution_json = move_line.analytic_distribution
|
||||
account_ids = [int(account_id) for key in distribution_json.keys() for account_id in key.split(',')]
|
||||
|
||||
sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', account_ids),
|
||||
('state', '=', 'sale')], order='create_date ASC', limit=1)
|
||||
if sale_order:
|
||||
mapping[move_line.id] = sale_order
|
||||
else:
|
||||
sale_order = self.env['sale.order'].search([('analytic_account_id', 'in', account_ids)],
|
||||
order='create_date ASC', limit=1)
|
||||
mapping[move_line.id] = sale_order
|
||||
|
||||
# map of AAL index with the SO on which it needs to be reinvoiced. Maybe be None if no SO found
|
||||
return mapping
|
||||
|
||||
def _sale_prepare_sale_line_values(self, order, price):
|
||||
""" Generate the sale.line creation value from the current move line """
|
||||
self.ensure_one()
|
||||
last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
|
||||
last_sequence = last_so_line.sequence + 1 if last_so_line else 100
|
||||
|
||||
fpos = order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id)
|
||||
product_taxes = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == order.company_id)
|
||||
taxes = fpos.map_tax(product_taxes)
|
||||
|
||||
return {
|
||||
'order_id': order.id,
|
||||
'name': self.name,
|
||||
'sequence': last_sequence,
|
||||
'price_unit': price,
|
||||
'tax_id': [x.id for x in taxes],
|
||||
'discount': 0.0,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom': self.product_uom_id.id,
|
||||
'product_uom_qty': 0.0,
|
||||
'is_expense': True,
|
||||
}
|
||||
|
||||
def _sale_get_invoice_price(self, order):
|
||||
""" Based on the current move line, compute the price to reinvoice the analytic line that is going to be created (so the
|
||||
price of the sale line).
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
unit_amount = self.quantity
|
||||
amount = (self.credit or 0.0) - (self.debit or 0.0)
|
||||
|
||||
if self.product_id.expense_policy == 'sales_price':
|
||||
return order.pricelist_id._get_product_price(
|
||||
self.product_id,
|
||||
1.0,
|
||||
uom=self.product_uom_id,
|
||||
date=order.date_order,
|
||||
)
|
||||
|
||||
uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_is_zero(unit_amount, precision_digits=uom_precision_digits):
|
||||
return 0.0
|
||||
|
||||
# Prevent unnecessary currency conversion that could be impacted by exchange rate
|
||||
# fluctuations
|
||||
if self.company_id.currency_id and amount and self.company_id.currency_id == order.currency_id:
|
||||
return self.company_id.currency_id.round(abs(amount / unit_amount))
|
||||
|
||||
price_unit = abs(amount / unit_amount)
|
||||
currency_id = self.company_id.currency_id
|
||||
if currency_id and currency_id != order.currency_id:
|
||||
price_unit = currency_id._convert(price_unit, order.currency_id, order.company_id, order.date_order or fields.Date.today())
|
||||
return price_unit
|
||||
|
||||
def _get_downpayment_lines(self):
|
||||
# OVERRIDE
|
||||
return self.sale_line_ids.filtered('is_downpayment').invoice_lines.filtered(lambda line: line.move_id._is_downpayment())
|
34
models/analytic.py
Normal file
34
models/analytic.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = "account.analytic.line"
|
||||
|
||||
# [XBO] TODO: remove me in master
|
||||
allowed_so_line_ids = fields.Many2many('sale.order.line', compute='_compute_allowed_so_line_ids')
|
||||
so_line = fields.Many2one('sale.order.line', string='Sales Order Item', domain=[('qty_delivered_method', '=', 'analytic')])
|
||||
|
||||
def _default_sale_line_domain(self):
|
||||
""" This is only used for delivered quantity of SO line based on analytic line, and timesheet
|
||||
(see sale_timesheet). This can be override to allow further customization.
|
||||
[XBO] TODO: remove me in master
|
||||
"""
|
||||
return [('qty_delivered_method', '=', 'analytic')]
|
||||
|
||||
def _compute_allowed_so_line_ids(self):
|
||||
# [XBO] TODO: remove me in master
|
||||
self.allowed_so_line_ids = False
|
||||
|
||||
class AccountAnalyticApplicability(models.Model):
|
||||
_inherit = 'account.analytic.applicability'
|
||||
_description = "Analytic Plan's Applicabilities"
|
||||
|
||||
business_domain = fields.Selection(
|
||||
selection_add=[
|
||||
('sale_order', 'Sale Order'),
|
||||
],
|
||||
ondelete={'sale_order': 'cascade'},
|
||||
)
|
166
models/crm_team.py
Normal file
166
models/crm_team.py
Normal file
@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class CrmTeam(models.Model):
|
||||
_inherit = 'crm.team'
|
||||
|
||||
invoiced = fields.Float(
|
||||
compute='_compute_invoiced',
|
||||
string='Invoiced This Month', readonly=True,
|
||||
help="Invoice revenue for the current month. This is the amount the sales "
|
||||
"channel has invoiced this month. It is used to compute the progression ratio "
|
||||
"of the current and target revenue on the kanban view.")
|
||||
invoiced_target = fields.Float(
|
||||
string='Invoicing Target',
|
||||
help="Revenue target for the current month (untaxed total of confirmed invoices).")
|
||||
quotations_count = fields.Integer(
|
||||
compute='_compute_quotations_to_invoice',
|
||||
string='Number of quotations to invoice', readonly=True)
|
||||
quotations_amount = fields.Float(
|
||||
compute='_compute_quotations_to_invoice',
|
||||
string='Amount of quotations to invoice', readonly=True)
|
||||
sales_to_invoice_count = fields.Integer(
|
||||
compute='_compute_sales_to_invoice',
|
||||
string='Number of sales to invoice', readonly=True)
|
||||
sale_order_count = fields.Integer(compute='_compute_sale_order_count', string='# Sale Orders')
|
||||
|
||||
def _compute_quotations_to_invoice(self):
|
||||
query = self.env['sale.order']._where_calc([
|
||||
('team_id', 'in', self.ids),
|
||||
('state', 'in', ['draft', 'sent']),
|
||||
])
|
||||
self.env['sale.order']._apply_ir_rules(query, 'read')
|
||||
_, where_clause, where_clause_args = query.get_sql()
|
||||
select_query = """
|
||||
SELECT team_id, count(*), sum(amount_total /
|
||||
CASE COALESCE(currency_rate, 0)
|
||||
WHEN 0 THEN 1.0
|
||||
ELSE currency_rate
|
||||
END
|
||||
) as amount_total
|
||||
FROM sale_order
|
||||
WHERE %s
|
||||
GROUP BY team_id
|
||||
""" % where_clause
|
||||
self.env.cr.execute(select_query, where_clause_args)
|
||||
quotation_data = self.env.cr.dictfetchall()
|
||||
teams = self.browse()
|
||||
for datum in quotation_data:
|
||||
team = self.browse(datum['team_id'])
|
||||
team.quotations_amount = datum['amount_total']
|
||||
team.quotations_count = datum['count']
|
||||
teams |= team
|
||||
remaining = (self - teams)
|
||||
remaining.quotations_amount = 0
|
||||
remaining.quotations_count = 0
|
||||
|
||||
def _compute_sales_to_invoice(self):
|
||||
sale_order_data = self.env['sale.order']._read_group([
|
||||
('team_id', 'in', self.ids),
|
||||
('invoice_status','=','to invoice'),
|
||||
], ['team_id'], ['__count'])
|
||||
data_map = {team.id: count for team, count in sale_order_data}
|
||||
for team in self:
|
||||
team.sales_to_invoice_count = data_map.get(team.id,0.0)
|
||||
|
||||
def _compute_invoiced(self):
|
||||
if not self:
|
||||
return
|
||||
|
||||
query = '''
|
||||
SELECT
|
||||
move.team_id AS team_id,
|
||||
SUM(move.amount_untaxed_signed) AS amount_untaxed_signed
|
||||
FROM account_move move
|
||||
WHERE move.move_type IN ('out_invoice', 'out_refund', 'out_receipt')
|
||||
AND move.payment_state IN ('in_payment', 'paid', 'reversed')
|
||||
AND move.state = 'posted'
|
||||
AND move.team_id IN %s
|
||||
AND move.date BETWEEN %s AND %s
|
||||
GROUP BY move.team_id
|
||||
'''
|
||||
today = fields.Date.today()
|
||||
params = [tuple(self.ids), fields.Date.to_string(today.replace(day=1)), fields.Date.to_string(today)]
|
||||
self._cr.execute(query, params)
|
||||
|
||||
data_map = dict((v[0], v[1]) for v in self._cr.fetchall())
|
||||
for team in self:
|
||||
team.invoiced = data_map.get(team.id, 0.0)
|
||||
|
||||
def _compute_sale_order_count(self):
|
||||
sale_order_data = self.env['sale.order']._read_group([
|
||||
('team_id', 'in', self.ids),
|
||||
('state', '!=', 'cancel'),
|
||||
], ['team_id'], ['__count'])
|
||||
data_map = {team.id: count for team, count in sale_order_data}
|
||||
for team in self:
|
||||
team.sale_order_count = data_map.get(team.id, 0)
|
||||
|
||||
def _in_sale_scope(self):
|
||||
return self.env.context.get('in_sales_app')
|
||||
|
||||
def _graph_get_model(self):
|
||||
if self._in_sale_scope():
|
||||
return 'sale.report'
|
||||
return super()._graph_get_model()
|
||||
|
||||
def _graph_date_column(self):
|
||||
if self._in_sale_scope():
|
||||
return 'date'
|
||||
return super()._graph_date_column()
|
||||
|
||||
def _graph_get_table(self, GraphModel):
|
||||
if self._in_sale_scope():
|
||||
# For a team not shared between company, we make sure the amounts are expressed
|
||||
# in the currency of the team company and not converted to the current company currency,
|
||||
# as the amounts of the sale report are converted in the currency
|
||||
# of the current company (for multi-company reporting, see #83550)
|
||||
GraphModel = GraphModel.with_company(self.company_id)
|
||||
return f"({GraphModel._table_query}) AS {GraphModel._table}"
|
||||
return super()._graph_get_table(GraphModel)
|
||||
|
||||
def _graph_y_query(self):
|
||||
if self._in_sale_scope():
|
||||
return 'SUM(price_subtotal)'
|
||||
return super()._graph_y_query()
|
||||
|
||||
def _extra_sql_conditions(self):
|
||||
if self._in_sale_scope():
|
||||
return "AND state = 'sale'"
|
||||
return super()._extra_sql_conditions()
|
||||
|
||||
def _graph_title_and_key(self):
|
||||
if self._in_sale_scope():
|
||||
return ['', _('Sales: Untaxed Total')] # no more title
|
||||
return super()._graph_title_and_key()
|
||||
|
||||
def _compute_dashboard_button_name(self):
|
||||
super(CrmTeam,self)._compute_dashboard_button_name()
|
||||
if self._in_sale_scope():
|
||||
self.dashboard_button_name = _("Sales Analysis")
|
||||
|
||||
def action_primary_channel_button(self):
|
||||
if self._in_sale_scope():
|
||||
return self.env["ir.actions.actions"]._for_xml_id("sale.action_order_report_so_salesteam")
|
||||
return super().action_primary_channel_button()
|
||||
|
||||
def update_invoiced_target(self, value):
|
||||
return self.write({'invoiced_target': round(float(value or 0))})
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_used_for_sales(self):
|
||||
""" If more than 5 active SOs, we consider this team to be actively used.
|
||||
5 is some random guess based on "user testing", aka more than testing
|
||||
CRM feature and less than use it in real life use cases. """
|
||||
SO_COUNT_TRIGGER = 5
|
||||
for team in self:
|
||||
if team.sale_order_count >= SO_COUNT_TRIGGER:
|
||||
raise UserError(
|
||||
_('Team %(team_name)s has %(sale_order_count)s active sale orders. Consider canceling them or archiving the team instead.',
|
||||
team_name=team.name,
|
||||
sale_order_count=team.sale_order_count
|
||||
))
|
12
models/onboarding_onboarding.py
Normal file
12
models/onboarding_onboarding.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class Onboarding(models.Model):
|
||||
_inherit = 'onboarding.onboarding'
|
||||
|
||||
# Sale Quotation Onboarding
|
||||
@api.model
|
||||
def action_close_panel_sale_quotation(self):
|
||||
self.action_close_panel('sale.onboarding_onboarding_sale_quotation')
|
98
models/onboarding_onboarding_step.py
Normal file
98
models/onboarding_onboarding_step.py
Normal file
@ -0,0 +1,98 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from odoo import _, api, Command, models
|
||||
from odoo.tools import file_open
|
||||
|
||||
|
||||
class OnboardingStep(models.Model):
|
||||
_inherit = 'onboarding.onboarding.step'
|
||||
|
||||
@api.model
|
||||
def action_open_step_sale_order_confirmation(self):
|
||||
self.env.company.get_chart_of_accounts_or_fail()
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Choose how to confirm quotations'),
|
||||
'res_model': 'sale.payment.provider.onboarding.wizard',
|
||||
'view_mode': 'form',
|
||||
'views': [(self.env.ref('payment.payment_provider_onboarding_wizard_form').id, 'form')],
|
||||
'target': 'new',
|
||||
}
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _get_sample_sales_order(self):
|
||||
""" Get a sample quotation or create one if it does not exist. """
|
||||
# use current user as partner
|
||||
partner = self.env.user.partner_id
|
||||
company_id = self.env.company.id
|
||||
# is there already one?
|
||||
sample_sales_order = self.env['sale.order'].search([
|
||||
('company_id', '=', company_id),
|
||||
('partner_id', '=', partner.id),
|
||||
('state', '=', 'draft'),
|
||||
], limit=1)
|
||||
if not sample_sales_order:
|
||||
# take any existing product or create one
|
||||
product = self.env['product.product'].search([], limit=1)
|
||||
if not product:
|
||||
with file_open('product/static/img/product_product_13-image.jpg', 'rb') as default_image_stream:
|
||||
product = self.env['product.product'].create({
|
||||
'name': _('Sample Product'),
|
||||
'active': False,
|
||||
'image_1920': base64.b64encode(default_image_stream.read()),
|
||||
})
|
||||
product.product_tmpl_id.active = False
|
||||
sample_sales_order = self.env['sale.order'].create({
|
||||
'partner_id': partner.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'name': _('Sample Order Line'),
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': 123,
|
||||
})
|
||||
]
|
||||
})
|
||||
return sample_sales_order
|
||||
|
||||
@api.model
|
||||
def action_open_step_sample_quotation(self):
|
||||
""" Onboarding step for sending a sample quotation. Open a window to compose an email,
|
||||
with the edi_invoice_template message loaded by default. """
|
||||
sample_sales_order = self._get_sample_sales_order()
|
||||
template = self.env.ref('sale.email_template_edi_sale', False)
|
||||
|
||||
self.env['mail.compose.message'].with_context(
|
||||
mark_so_as_sent=True,
|
||||
default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature',
|
||||
proforma=self.env.context.get('proforma', False),
|
||||
).create({
|
||||
'res_ids': sample_sales_order.ids,
|
||||
'template_id': template.id if template else False,
|
||||
'model': sample_sales_order._name,
|
||||
'composition_mode': 'comment',
|
||||
})._action_send_mail()
|
||||
|
||||
self.action_validate_step('sale.onboarding_onboarding_step_sample_quotation')
|
||||
sale_quotation_onboarding = self.env.ref('sale.onboarding_onboarding_sale_quotation', raise_if_not_found=False)
|
||||
if sale_quotation_onboarding:
|
||||
sale_quotation_onboarding.action_close()
|
||||
|
||||
view_id = self.env.ref('sale.view_order_form').id
|
||||
action = self.env['ir.actions.actions']._for_xml_id('sale.action_orders')
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'views': [[view_id, 'form']],
|
||||
'target': 'main',
|
||||
})
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def action_validate_step_payment_provider(self):
|
||||
validation_response = super().action_validate_step_payment_provider()
|
||||
if self.env.company.sale_onboarding_payment_method: # Set if the flow is/was done from the Sales panel
|
||||
return self.action_validate_step('sale.onboarding_onboarding_step_sale_order_confirmation')
|
||||
return validation_response
|
15
models/payment_provider.py
Normal file
15
models/payment_provider.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PaymentProvider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
so_reference_type = fields.Selection(string='Communication',
|
||||
selection=[
|
||||
('so_name', 'Based on Document Reference'),
|
||||
('partner', 'Based on Customer ID')], default='so_name',
|
||||
help='You can set here the communication type that will appear on sales orders.'
|
||||
'The communication will be given to the customer when they choose the payment method.')
|
235
models/payment_transaction.py
Normal file
235
models/payment_transaction.py
Normal file
@ -0,0 +1,235 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil import relativedelta
|
||||
|
||||
from odoo import _, api, Command, fields, models, SUPERUSER_ID
|
||||
from odoo.tools import str2bool
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
sale_order_ids = fields.Many2many('sale.order', 'sale_order_transaction_rel', 'transaction_id', 'sale_order_id',
|
||||
string='Sales Orders', copy=False, readonly=True)
|
||||
sale_order_ids_nbr = fields.Integer(compute='_compute_sale_order_ids_nbr', string='# of Sales Orders')
|
||||
|
||||
def _compute_sale_order_reference(self, order):
|
||||
self.ensure_one()
|
||||
if self.provider_id.so_reference_type == 'so_name':
|
||||
order_reference = order.name
|
||||
else:
|
||||
# self.provider_id.so_reference_type == 'partner'
|
||||
identification_number = order.partner_id.id
|
||||
order_reference = '%s/%s' % ('CUST', str(identification_number % 97).rjust(2, '0'))
|
||||
|
||||
invoice_journal = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
if invoice_journal:
|
||||
order_reference = invoice_journal._process_reference_for_sale_order(order_reference)
|
||||
|
||||
return order_reference
|
||||
|
||||
@api.depends('sale_order_ids')
|
||||
def _compute_sale_order_ids_nbr(self):
|
||||
for trans in self:
|
||||
trans.sale_order_ids_nbr = len(trans.sale_order_ids)
|
||||
|
||||
def _set_pending(self, state_message=None, **kwargs):
|
||||
""" Override of `payment` to send the quotations automatically.
|
||||
|
||||
:param str state_message: The reason for which the transaction is set in 'pending' state.
|
||||
:return: updated transactions.
|
||||
:rtype: `payment.transaction` recordset.
|
||||
"""
|
||||
txs_to_process = super()._set_pending(state_message=state_message, **kwargs)
|
||||
|
||||
for tx in txs_to_process: # Consider only transactions that are indeed set pending.
|
||||
sales_orders = tx.sale_order_ids.filtered(lambda so: so.state in ['draft', 'sent'])
|
||||
sales_orders.filtered(
|
||||
lambda so: so.state == 'draft'
|
||||
).with_context(tracking_disable=True).action_quotation_sent()
|
||||
|
||||
if tx.provider_id.code == 'custom':
|
||||
for so in tx.sale_order_ids:
|
||||
so.reference = tx._compute_sale_order_reference(so)
|
||||
|
||||
# Send the payment status email.
|
||||
# The transactions are manually cached while in a sudoed environment to prevent an
|
||||
# AccessError: In some circumstances, sending the mail would generate the report assets
|
||||
# during the rendering of the mail body, causing a cursor commit, a flush, and forcing
|
||||
# the re-computation of the pending computed fields of the `mail.compose.message`,
|
||||
# including part of the template. Since that template reads the order's transactions and
|
||||
# the re-computation of the field is not done with the same environment, reading fields
|
||||
# that were not already available in the cache could trigger an AccessError (e.g., if
|
||||
# the payment was initiated by a public user).
|
||||
sales_orders.mapped('transaction_ids')
|
||||
sales_orders._send_payment_succeeded_for_order_mail()
|
||||
|
||||
return txs_to_process
|
||||
|
||||
def _check_amount_and_confirm_order(self):
|
||||
""" Confirm the sales order based on the amount of a transaction.
|
||||
|
||||
Confirm the sales orders only if the transaction amount (or the sum of the partial
|
||||
transaction amounts) is equal to or greater than the required amount for order confirmation
|
||||
|
||||
Grouped payments (paying multiple sales orders in one transaction) are not supported.
|
||||
|
||||
:return: The confirmed sales orders.
|
||||
:rtype: a `sale.order` recordset
|
||||
"""
|
||||
confirmed_orders = self.env['sale.order']
|
||||
for tx in self:
|
||||
# We only support the flow where exactly one quotation is linked to a transaction.
|
||||
if len(tx.sale_order_ids) == 1:
|
||||
quotation = tx.sale_order_ids.filtered(lambda so: so.state in ('draft', 'sent'))
|
||||
if quotation and quotation._is_confirmation_amount_reached():
|
||||
quotation.with_context(send_email=True).action_confirm()
|
||||
confirmed_orders |= quotation
|
||||
return confirmed_orders
|
||||
|
||||
def _set_authorized(self, state_message=None, **kwargs):
|
||||
""" Override of payment to confirm the quotations automatically. """
|
||||
super()._set_authorized(state_message=state_message, **kwargs)
|
||||
confirmed_orders = self._check_amount_and_confirm_order()
|
||||
confirmed_orders._send_order_confirmation_mail()
|
||||
(self.sale_order_ids - confirmed_orders)._send_payment_succeeded_for_order_mail()
|
||||
|
||||
def _log_message_on_linked_documents(self, message):
|
||||
""" Override of payment to log a message on the sales orders linked to the transaction.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param str message: The message to be logged
|
||||
:return: None
|
||||
"""
|
||||
super()._log_message_on_linked_documents(message)
|
||||
self = self.with_user(SUPERUSER_ID) # Log messages as 'OdooBot'
|
||||
for order in self.sale_order_ids or self.source_transaction_id.sale_order_ids:
|
||||
order.message_post(body=message)
|
||||
|
||||
def _reconcile_after_done(self):
|
||||
""" Override of payment to automatically confirm quotations and generate invoices. """
|
||||
confirmed_orders = self._check_amount_and_confirm_order()
|
||||
confirmed_orders._send_order_confirmation_mail()
|
||||
(self.sale_order_ids - confirmed_orders)._send_payment_succeeded_for_order_mail()
|
||||
|
||||
auto_invoice = str2bool(
|
||||
self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'))
|
||||
if auto_invoice:
|
||||
# Invoice the sale orders in self instead of in confirmed_orders to create the invoice
|
||||
# even if only a partial payment was made.
|
||||
self._invoice_sale_orders()
|
||||
super()._reconcile_after_done()
|
||||
if auto_invoice:
|
||||
# Must be called after the super() call to make sure the invoice are correctly posted.
|
||||
self._send_invoice()
|
||||
|
||||
def _send_invoice(self):
|
||||
template_id = int(self.env['ir.config_parameter'].sudo().get_param(
|
||||
'sale.default_invoice_email_template',
|
||||
default=0
|
||||
))
|
||||
if not template_id:
|
||||
return
|
||||
template = self.env['mail.template'].browse(template_id).exists()
|
||||
if not template:
|
||||
return
|
||||
|
||||
for tx in self:
|
||||
tx = tx.with_company(tx.company_id).with_context(
|
||||
company_id=tx.company_id.id,
|
||||
)
|
||||
invoice_to_send = tx.invoice_ids.filtered(
|
||||
lambda i: not i.is_move_sent and i.state == 'posted' and i._is_ready_to_be_sent()
|
||||
)
|
||||
invoice_to_send.is_move_sent = True # Mark invoice as sent
|
||||
invoice_to_send.with_user(SUPERUSER_ID)._generate_pdf_and_send_invoice(template)
|
||||
|
||||
def _cron_send_invoice(self):
|
||||
"""
|
||||
Cron to send invoice that where not ready to be send directly after posting
|
||||
"""
|
||||
if not self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
|
||||
return
|
||||
|
||||
# No need to retrieve old transactions
|
||||
retry_limit_date = datetime.now() - relativedelta.relativedelta(days=2)
|
||||
# Retrieve all transactions matching the criteria for post-processing
|
||||
self.search([
|
||||
('state', '=', 'done'),
|
||||
('is_post_processed', '=', True),
|
||||
('invoice_ids', 'in', self.env['account.move']._search([
|
||||
('is_move_sent', '=', False),
|
||||
('state', '=', 'posted'),
|
||||
])),
|
||||
('sale_order_ids.state', '=', 'sale'),
|
||||
('last_state_change', '>=', retry_limit_date),
|
||||
])._send_invoice()
|
||||
|
||||
def _invoice_sale_orders(self):
|
||||
for tx in self.filtered(lambda tx: tx.sale_order_ids):
|
||||
tx = tx.with_company(tx.company_id)
|
||||
|
||||
confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state == 'sale')
|
||||
if confirmed_orders:
|
||||
# Filter orders between those fully paid and those partially paid.
|
||||
fully_paid_orders = confirmed_orders.filtered(lambda so: so._is_paid())
|
||||
|
||||
# Create a down payment invoice for partially paid orders
|
||||
downpayment_invoices = (
|
||||
confirmed_orders - fully_paid_orders
|
||||
)._generate_downpayment_invoices()
|
||||
|
||||
# For fully paid orders create a final invoice.
|
||||
fully_paid_orders._force_lines_to_invoice_policy_order()
|
||||
final_invoices = fully_paid_orders.with_context(
|
||||
raise_if_nothing_to_invoice=False
|
||||
)._create_invoices(final=True)
|
||||
invoices = downpayment_invoices + final_invoices
|
||||
|
||||
# Setup access token in advance to avoid serialization failure between
|
||||
# edi postprocessing of invoice and displaying the sale order on the portal
|
||||
for invoice in invoices:
|
||||
invoice._portal_ensure_token()
|
||||
tx.invoice_ids = [Command.set(invoices.ids)]
|
||||
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, provider_code, separator, **values):
|
||||
""" Override of payment to compute the reference prefix based on Sales-specific values.
|
||||
|
||||
If the `values` parameter has an entry with 'sale_order_ids' as key and a list of (4, id, O)
|
||||
or (6, 0, ids) X2M command as value, the prefix is computed based on the sales order name(s)
|
||||
Otherwise, the computation is delegated to the super method.
|
||||
|
||||
:param str provider_code: The code of the provider handling the transaction
|
||||
:param str separator: The custom separator used to separate data references
|
||||
:param dict values: The transaction values used to compute the reference prefix. It should
|
||||
have the structure {'sale_order_ids': [(X2M command), ...], ...}.
|
||||
:return: The computed reference prefix if order ids are found, the one of `super` otherwise
|
||||
:rtype: str
|
||||
"""
|
||||
command_list = values.get('sale_order_ids')
|
||||
if command_list:
|
||||
# Extract sales order id(s) from the X2M commands
|
||||
order_ids = self._fields['sale_order_ids'].convert_to_cache(command_list, self)
|
||||
orders = self.env['sale.order'].browse(order_ids).exists()
|
||||
if len(orders) == len(order_ids): # All ids are valid
|
||||
return separator.join(orders.mapped('name'))
|
||||
return super()._compute_reference_prefix(provider_code, separator, **values)
|
||||
|
||||
def action_view_sales_orders(self):
|
||||
action = {
|
||||
'name': _('Sales Order(s)'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'sale.order',
|
||||
'target': 'current',
|
||||
}
|
||||
sale_order_ids = self.sale_order_ids.ids
|
||||
if len(sale_order_ids) == 1:
|
||||
action['res_id'] = sale_order_ids[0]
|
||||
action['view_mode'] = 'form'
|
||||
else:
|
||||
action['view_mode'] = 'tree,form'
|
||||
action['domain'] = [('id', 'in', sale_order_ids)]
|
||||
return action
|
22
models/product_document.py
Normal file
22
models/product_document.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductDocument(models.Model):
|
||||
_inherit = 'product.document'
|
||||
|
||||
attached_on = fields.Selection(
|
||||
selection=[
|
||||
('quotation', "Quotation"),
|
||||
('sale_order', "Confirmed order"),
|
||||
],
|
||||
string="Visible at",
|
||||
help="Allows you to share the document with your customers within a sale.\n"
|
||||
"Leave it empty if you don't want to share this document with sales customer.\n"
|
||||
"Quotation: the document will be sent to and accessible by customers at any time.\n"
|
||||
"e.g. this option can be useful to share Product description files.\n"
|
||||
"Confirmed order: the document will be sent to and accessible by customers.\n"
|
||||
"e.g. this option can be useful to share User Manual or digital content bought"
|
||||
" on ecommerce. ",
|
||||
)
|
111
models/product_product.py
Normal file
111
models/product_product.py
Normal file
@ -0,0 +1,111 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import time, timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_round
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
sales_count = fields.Float(compute='_compute_sales_count', string='Sold', digits='Product Unit of Measure')
|
||||
|
||||
# Catalog related fields
|
||||
product_catalog_product_is_in_sale_order = fields.Boolean(
|
||||
compute='_compute_product_is_in_sale_order',
|
||||
search='_search_product_is_in_sale_order',
|
||||
)
|
||||
|
||||
def _compute_sales_count(self):
|
||||
r = {}
|
||||
self.sales_count = 0
|
||||
if not self.user_has_groups('sales_team.group_sale_salesman'):
|
||||
return r
|
||||
date_from = fields.Datetime.to_string(fields.datetime.combine(fields.datetime.now() - timedelta(days=365),
|
||||
time.min))
|
||||
|
||||
done_states = self.env['sale.report']._get_done_states()
|
||||
|
||||
domain = [
|
||||
('state', 'in', done_states),
|
||||
('product_id', 'in', self.ids),
|
||||
('date', '>=', date_from),
|
||||
]
|
||||
for product, product_uom_qty in self.env['sale.report']._read_group(domain, ['product_id'], ['product_uom_qty:sum']):
|
||||
r[product.id] = product_uom_qty
|
||||
for product in self:
|
||||
if not product.id:
|
||||
product.sales_count = 0.0
|
||||
continue
|
||||
product.sales_count = float_round(r.get(product.id, 0), precision_rounding=product.uom_id.rounding)
|
||||
return r
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
if self._origin and self.sales_count > 0:
|
||||
return {'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _("You cannot change the product's type because it is already used in sales orders.")
|
||||
}}
|
||||
|
||||
@api.depends_context('order_id')
|
||||
def _compute_product_is_in_sale_order(self):
|
||||
order_id = self.env.context.get('order_id')
|
||||
if not order_id:
|
||||
self.product_catalog_product_is_in_sale_order = False
|
||||
return
|
||||
|
||||
read_group_data = self.env['sale.order.line']._read_group(
|
||||
domain=[('order_id', '=', order_id)],
|
||||
groupby=['product_id'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
data = {product.id: count for product, count in read_group_data}
|
||||
for product in self:
|
||||
product.product_catalog_product_is_in_sale_order = bool(data.get(product.id, 0))
|
||||
|
||||
def _search_product_is_in_sale_order(self, operator, value):
|
||||
if operator not in ['=', '!='] or not isinstance(value, bool):
|
||||
raise UserError(_("Operation not supported"))
|
||||
product_ids = self.env['sale.order.line'].search([
|
||||
('order_id', 'in', [self.env.context.get('order_id', '')]),
|
||||
]).product_id.ids
|
||||
return [('id', 'in', product_ids)]
|
||||
|
||||
def action_view_sales(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("sale.report_all_channels_sales_action")
|
||||
action['domain'] = [('product_id', 'in', self.ids)]
|
||||
action['context'] = {
|
||||
'pivot_measures': ['product_uom_qty'],
|
||||
'active_id': self._context.get('active_id'),
|
||||
'search_default_Sales': 1,
|
||||
'active_model': 'sale.report',
|
||||
'search_default_filter_order_date': 1,
|
||||
}
|
||||
return action
|
||||
|
||||
def _get_invoice_policy(self):
|
||||
return self.invoice_policy
|
||||
|
||||
def _filter_to_unlink(self):
|
||||
domain = [('product_id', 'in', self.ids)]
|
||||
lines = self.env['sale.order.line']._read_group(domain, ['product_id'])
|
||||
linked_product_ids = [product.id for [product] in lines]
|
||||
return super(ProductProduct, self - self.browse(linked_product_ids))._filter_to_unlink()
|
||||
|
||||
|
||||
class ProductAttributeCustomValue(models.Model):
|
||||
_inherit = "product.attribute.custom.value"
|
||||
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string="Sales Order Line", ondelete='cascade')
|
||||
|
||||
_sql_constraints = [
|
||||
('sol_custom_value_unique', 'unique(custom_product_template_attribute_value_id, sale_order_line_id)', "Only one Custom Value is allowed per Attribute Value per Sales Order Line.")
|
||||
]
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
|
||||
sales = fields.Boolean("Sales", default=True, help="If true, the packaging can be used for sales orders")
|
144
models/product_template.py
Normal file
144
models/product_template.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.float_utils import float_round
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
service_type = fields.Selection(
|
||||
selection=[('manual', "Manually set quantities on order")],
|
||||
string="Track Service",
|
||||
compute='_compute_service_type', store=True, readonly=False, precompute=True,
|
||||
help="Manually set quantities on order: Invoice based on the manually entered quantity, without creating an analytic account.\n"
|
||||
"Timesheets on contract: Invoice based on the tracked hours on the related timesheet.\n"
|
||||
"Create a task and track hours: Create a task on the sales order validation and track the work hours.")
|
||||
sale_line_warn = fields.Selection(
|
||||
WARNING_MESSAGE, string="Sales Order Line",
|
||||
help=WARNING_HELP, required=True, default="no-message")
|
||||
sale_line_warn_msg = fields.Text(string="Message for Sales Order Line")
|
||||
expense_policy = fields.Selection(
|
||||
selection=[
|
||||
('no', "No"),
|
||||
('cost', "At cost"),
|
||||
('sales_price', "Sales price"),
|
||||
],
|
||||
string="Re-Invoice Expenses", default='no',
|
||||
compute='_compute_expense_policy', store=True, readonly=False,
|
||||
help="Validated expenses and vendor bills can be re-invoiced to a customer at its cost or sales price.")
|
||||
visible_expense_policy = fields.Boolean(
|
||||
string="Re-Invoice Policy visible", compute='_compute_visible_expense_policy')
|
||||
sales_count = fields.Float(
|
||||
string="Sold", compute='_compute_sales_count', digits='Product Unit of Measure')
|
||||
invoice_policy = fields.Selection(
|
||||
selection=[
|
||||
('order', "Ordered quantities"),
|
||||
('delivery', "Delivered quantities"),
|
||||
],
|
||||
string="Invoicing Policy",
|
||||
compute='_compute_invoice_policy', store=True, readonly=False, precompute=True,
|
||||
help="Ordered Quantity: Invoice quantities ordered by the customer.\n"
|
||||
"Delivered Quantity: Invoice quantities delivered to the customer.")
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_visible_expense_policy(self):
|
||||
visibility = self.user_has_groups('analytic.group_analytic_accounting')
|
||||
for product_template in self:
|
||||
product_template.visible_expense_policy = visibility
|
||||
|
||||
@api.depends('sale_ok')
|
||||
def _compute_expense_policy(self):
|
||||
self.filtered(lambda t: not t.sale_ok).expense_policy = 'no'
|
||||
|
||||
@api.depends('product_variant_ids.sales_count')
|
||||
def _compute_sales_count(self):
|
||||
for product in self:
|
||||
product.sales_count = float_round(sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]), precision_rounding=product.uom_id.rounding)
|
||||
|
||||
@api.constrains('company_id')
|
||||
def _check_sale_product_company(self):
|
||||
"""Ensure the product is not being restricted to a single company while
|
||||
having been sold in another one in the past, as this could cause issues."""
|
||||
target_company = self.company_id
|
||||
if target_company: # don't prevent writing `False`, should always work
|
||||
subquery_products = self.env['product.product'].sudo().with_context(active_test=False)._search([('product_tmpl_id', 'in', self.ids)])
|
||||
so_lines = self.env['sale.order.line'].sudo().search_read(
|
||||
[('product_id', 'in', subquery_products), '!', ('company_id', 'child_of', target_company.root_id.id)],
|
||||
fields=['id', 'product_id'],
|
||||
)
|
||||
used_products = list(map(lambda sol: sol['product_id'][1], so_lines))
|
||||
if so_lines:
|
||||
raise ValidationError(_('The following products cannot be restricted to the company'
|
||||
' %s because they have already been used in quotations or '
|
||||
'sales orders in another company:\n%s\n'
|
||||
'You can archive these products and recreate them '
|
||||
'with your company restriction instead, or leave them as '
|
||||
'shared product.', target_company.name, ', '.join(used_products)))
|
||||
|
||||
def action_view_sales(self):
|
||||
action = self.env['ir.actions.actions']._for_xml_id('sale.report_all_channels_sales_action')
|
||||
action['domain'] = [('product_tmpl_id', 'in', self.ids)]
|
||||
action['context'] = {
|
||||
'pivot_measures': ['product_uom_qty'],
|
||||
'active_id': self._context.get('active_id'),
|
||||
'active_model': 'sale.report',
|
||||
'search_default_Sales': 1,
|
||||
'search_default_filter_order_date': 1,
|
||||
}
|
||||
return action
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
res = super(ProductTemplate, self)._onchange_type()
|
||||
if self._origin and self.sales_count > 0:
|
||||
res['warning'] = {
|
||||
'title': _("Warning"),
|
||||
'message': _("You cannot change the product's type because it is already used in sales orders.")
|
||||
}
|
||||
return res
|
||||
|
||||
@api.depends('type')
|
||||
def _compute_service_type(self):
|
||||
self.filtered(lambda t: t.type == 'consu' or not t.service_type).service_type = 'manual'
|
||||
|
||||
@api.depends('type')
|
||||
def _compute_invoice_policy(self):
|
||||
self.filtered(lambda t: t.type == 'consu' or not t.invoice_policy).invoice_policy = 'order'
|
||||
|
||||
@api.model
|
||||
def get_import_templates(self):
|
||||
res = super(ProductTemplate, self).get_import_templates()
|
||||
if self.env.context.get('sale_multi_pricelist_product_template'):
|
||||
if self.user_has_groups('product.group_sale_pricelist'):
|
||||
return [{
|
||||
'label': _("Import Template for Products"),
|
||||
'template': '/product/static/xls/product_template.xls'
|
||||
}]
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_incompatible_types(self):
|
||||
return []
|
||||
|
||||
@api.constrains(lambda self: self._get_incompatible_types())
|
||||
def _check_incompatible_types(self):
|
||||
incompatible_types = self._get_incompatible_types()
|
||||
if len(incompatible_types) < 2:
|
||||
return
|
||||
fields = self.env['ir.model.fields'].sudo().search_read(
|
||||
[('model', '=', 'product.template'), ('name', 'in', incompatible_types)],
|
||||
['name', 'field_description'])
|
||||
field_descriptions = {v['name']: v['field_description'] for v in fields}
|
||||
field_list = incompatible_types + ['name']
|
||||
values = self.read(field_list)
|
||||
for val in values:
|
||||
incompatible_fields = [f for f in incompatible_types if val[f]]
|
||||
if len(incompatible_fields) > 1:
|
||||
raise ValidationError(_(
|
||||
"The product (%s) has incompatible values: %s",
|
||||
val['name'],
|
||||
','.join(field_descriptions[v] for v in incompatible_fields),
|
||||
))
|
66
models/res_company.py
Normal file
66
models/res_company.py
Normal file
@ -0,0 +1,66 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
_check_company_auto = True
|
||||
|
||||
_sql_constraints = [
|
||||
('check_quotation_validity_days',
|
||||
'CHECK(quotation_validity_days >= 0)',
|
||||
"You cannot set a negative number for the default quotation validity."
|
||||
" Leave empty (or 0) to disable the automatic expiration of quotations."),
|
||||
]
|
||||
|
||||
portal_confirmation_sign = fields.Boolean(string="Online Signature", default=True)
|
||||
portal_confirmation_pay = fields.Boolean(string="Online Payment")
|
||||
prepayment_percent = fields.Float(
|
||||
string="Prepayment percentage",
|
||||
default=1.0,
|
||||
help="The percentage of the amount needed to be paid to confirm quotations.")
|
||||
quotation_validity_days = fields.Integer(
|
||||
string="Default Quotation Validity",
|
||||
default=30,
|
||||
help="Days between quotation proposal and expiration."
|
||||
" 0 days means automatic expiration is disabled",
|
||||
)
|
||||
sale_discount_product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
string="Discount Product",
|
||||
domain=[
|
||||
('type', '=', 'service'),
|
||||
('invoice_policy', '=', 'order'),
|
||||
],
|
||||
help="Default product used for discounts",
|
||||
check_company=True,
|
||||
)
|
||||
sale_down_payment_product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
string="Deposit Product",
|
||||
domain=[
|
||||
('type', '=', 'service'),
|
||||
('invoice_policy', '=', 'order'),
|
||||
],
|
||||
help="Default product used for down payments",
|
||||
check_company=True,
|
||||
)
|
||||
|
||||
# sale onboarding
|
||||
sale_onboarding_payment_method = fields.Selection(
|
||||
selection=[
|
||||
('digital_signature', "Sign online"),
|
||||
('paypal', "PayPal"),
|
||||
('stripe', "Stripe"),
|
||||
('other', "Pay with another payment provider"),
|
||||
('manual', "Manual Payment"),
|
||||
],
|
||||
string="Sale onboarding selected payment method")
|
||||
|
||||
@api.constrains('prepayment_percent')
|
||||
def _check_prepayment_percent(self):
|
||||
for company in self:
|
||||
if company.portal_confirmation_pay and not (0 < company.prepayment_percent <= 1.0):
|
||||
raise ValidationError(_("Prepayment percentage must be a valid percentage."))
|
91
models/res_partner.py
Normal file
91
models/res_partner.py
Normal file
@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
|
||||
from odoo.osv import expression
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
sale_order_count = fields.Integer(compute='_compute_sale_order_count', string='Sale Order Count')
|
||||
sale_order_ids = fields.One2many('sale.order', 'partner_id', 'Sales Order')
|
||||
sale_warn = fields.Selection(WARNING_MESSAGE, 'Sales Warnings', default='no-message', help=WARNING_HELP)
|
||||
sale_warn_msg = fields.Text('Message for Sales Order')
|
||||
|
||||
@api.model
|
||||
def _get_sale_order_domain_count(self):
|
||||
return []
|
||||
|
||||
def _compute_sale_order_count(self):
|
||||
# retrieve all children partners and prefetch 'parent_id' on them
|
||||
all_partners = self.with_context(active_test=False).search_fetch(
|
||||
[('id', 'child_of', self.ids)],
|
||||
['parent_id'],
|
||||
)
|
||||
sale_order_groups = self.env['sale.order']._read_group(
|
||||
domain=expression.AND([self._get_sale_order_domain_count(), [('partner_id', 'in', all_partners.ids)]]),
|
||||
groupby=['partner_id'], aggregates=['__count']
|
||||
)
|
||||
self_ids = set(self._ids)
|
||||
|
||||
self.sale_order_count = 0
|
||||
for partner, count in sale_order_groups:
|
||||
while partner:
|
||||
if partner.id in self_ids:
|
||||
partner.sale_order_count += count
|
||||
partner = partner.parent_id
|
||||
|
||||
def _has_order(self, partner_domain):
|
||||
self.ensure_one()
|
||||
sale_order = self.env['sale.order'].sudo().search(
|
||||
expression.AND([
|
||||
partner_domain,
|
||||
[
|
||||
('state', 'in', ('sent', 'sale')),
|
||||
]
|
||||
]),
|
||||
limit=1,
|
||||
)
|
||||
return bool(sale_order)
|
||||
|
||||
def _can_edit_name(self):
|
||||
""" Can't edit `name` if there is (non draft) issued SO. """
|
||||
return super()._can_edit_name() and not self._has_order(
|
||||
[
|
||||
('partner_invoice_id', '=', self.id),
|
||||
('partner_id', '=', self.id),
|
||||
]
|
||||
)
|
||||
|
||||
def can_edit_vat(self):
|
||||
""" Can't edit `vat` if there is (non draft) issued SO. """
|
||||
return super().can_edit_vat() and not self._has_order(
|
||||
[('partner_id', 'child_of', self.commercial_partner_id.id)]
|
||||
)
|
||||
|
||||
def action_view_sale_order(self):
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('sale.act_res_partner_2_sale_order')
|
||||
all_child = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
action["domain"] = [("partner_id", "in", all_child.ids)]
|
||||
return action
|
||||
|
||||
def _compute_credit_to_invoice(self):
|
||||
# EXTENDS 'account'
|
||||
super()._compute_credit_to_invoice()
|
||||
domain = [('partner_id', 'in', self.ids), ('state', '=', 'sale')]
|
||||
group = self.env['sale.order']._read_group(domain, ['partner_id'], ['amount_to_invoice:sum'])
|
||||
for partner, amount_to_invoice_sum in group:
|
||||
partner.credit_to_invoice += amount_to_invoice_sum
|
||||
|
||||
|
||||
def unlink(self):
|
||||
# Unlink draft/cancelled SO so that the partner can be removed from database
|
||||
self.env['sale.order'].sudo().search([
|
||||
('state', 'in', ['draft', 'cancel']),
|
||||
'|', '|',
|
||||
('partner_id', 'in', self.ids),
|
||||
('partner_invoice_id', 'in', self.ids),
|
||||
('partner_shipping_id', 'in', self.ids),
|
||||
]).unlink()
|
||||
return super().unlink()
|
1915
models/sale_order.py
Normal file
1915
models/sale_order.py
Normal file
File diff suppressed because it is too large
Load Diff
1270
models/sale_order_line.py
Normal file
1270
models/sale_order_line.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user