From 94716a3f14d929574c46ef165e4364d12a5903ed Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli <nim@odoo.com> Date: Thu, 27 Aug 2015 14:20:02 +0200 Subject: [PATCH] [IMP] sale: adaptation due to the new Sale module Invoicing is now completely handled from the SO. cf. documentation Reason: complete rewrite of the Sale module. Responsible: fp, dbo, nim --- addons/sale/__init__.py | 1 + addons/sale/__openerp__.py | 16 +- addons/sale/report/invoice_report.py | 18 +- addons/sale/report/sale_report.py | 42 +- addons/sale/report/sale_report_view.xml | 3 +- addons/sale/res_config.py | 23 +- addons/sale/res_config_view.xml | 4 +- addons/sale/res_partner_view.xml | 2 +- addons/sale/sale.py | 1756 ++++++----------- addons/sale/sale_analytic.py | 108 + addons/sale/sale_demo.xml | 33 +- addons/sale/sale_product_demo.xml | 273 +++ addons/sale/sale_unit_test.xml | 2 - addons/sale/sale_view.xml | 353 ++-- addons/sale/sale_workflow.xml | 303 --- addons/sale/test/cancel_order.yml | 82 - addons/sale/test/canceled_lines_order.yml | 60 - addons/sale/test/create_sale_users.yml | 28 - addons/sale/test/delete_order.yml | 19 - addons/sale/test/manual_order_policy.yml | 66 - addons/sale/test/sale_order_demo.yml | 46 - addons/sale/tests/__init__.py | 4 +- addons/sale/tests/test_sale_common.py | 38 + addons/sale/tests/test_sale_order.py | 124 ++ addons/sale/tests/test_sale_to_invoice.py | 11 +- addons/sale/views/report_saleorder.xml | 46 +- addons/sale/wizard/__init__.py | 2 - addons/sale/wizard/sale_line_invoice.py | 119 -- addons/sale/wizard/sale_line_invoice.xml | 42 - addons/sale/wizard/sale_make_invoice.py | 52 - addons/sale/wizard/sale_make_invoice.xml | 42 - .../sale/wizard/sale_make_invoice_advance.py | 306 ++- .../sale/wizard/sale_make_invoice_advance.xml | 44 +- 33 files changed, 1545 insertions(+), 2523 deletions(-) create mode 100644 addons/sale/sale_analytic.py create mode 100644 addons/sale/sale_product_demo.xml delete mode 100644 addons/sale/sale_workflow.xml delete mode 100644 addons/sale/test/cancel_order.yml delete mode 100644 addons/sale/test/canceled_lines_order.yml delete mode 100644 addons/sale/test/create_sale_users.yml delete mode 100644 addons/sale/test/delete_order.yml delete mode 100644 addons/sale/test/manual_order_policy.yml delete mode 100644 addons/sale/test/sale_order_demo.yml create mode 100644 addons/sale/tests/test_sale_common.py create mode 100644 addons/sale/tests/test_sale_order.py delete mode 100644 addons/sale/wizard/sale_line_invoice.py delete mode 100644 addons/sale/wizard/sale_line_invoice.xml delete mode 100644 addons/sale/wizard/sale_make_invoice.py delete mode 100644 addons/sale/wizard/sale_make_invoice.xml diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py index 3c0854c48bc1..2ec8f5b6b2f9 100644 --- a/addons/sale/__init__.py +++ b/addons/sale/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import sale +import sale_analytic import sales_team import res_partner import wizard diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index 9efb1c43615f..3cb0f397a663 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -42,11 +42,8 @@ The Dashboard for the Sales Manager will include 'depends': ['sales_team','account', 'procurement', 'report'], 'data': [ 'wizard/sale_make_invoice_advance.xml', - 'wizard/sale_line_invoice.xml', - 'wizard/sale_make_invoice.xml', 'security/sale_security.xml', 'security/ir.model.access.csv', - 'sale_workflow.xml', 'sale_sequence.xml', 'sale_report.xml', 'sale_data.xml', @@ -61,16 +58,9 @@ The Dashboard for the Sales Manager will include 'sales_team_dashboard.xml', 'sale_tip_data.xml', ], - 'demo': ['sale_demo.xml'], - 'test': [ - '../account/test/account_minimal_test.xml', - 'test/create_sale_users.yml', - 'test/sale_order_demo.yml', - 'test/manual_order_policy.yml', - 'test/cancel_order.yml', - 'test/delete_order.yml', - 'test/canceled_lines_order.yml', - ], + 'demo': ['sale_demo.xml', + 'sale_product_demo.xml', + ], 'css': ['static/src/css/sale.css'], 'installable': True, 'auto_install': False, diff --git a/addons/sale/report/invoice_report.py b/addons/sale/report/invoice_report.py index 315896b2dc92..d38931789367 100644 --- a/addons/sale/report/invoice_report.py +++ b/addons/sale/report/invoice_report.py @@ -1,21 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields,osv -class account_invoice_report(osv.osv): +from openerp import api, fields, models, _ + +class AccountInvoiceReport(models.Model): _inherit = 'account.invoice.report' - _columns = { - 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'), - } - _depends = { - 'account.invoice': ['team_id'], - } + team_id = fields.Many2one('crm.team', string='Sales Team') def _select(self): - return super(account_invoice_report, self)._select() + ", sub.team_id as team_id" + return super(AccountInvoiceReport, self)._select() + ", sub.team_id as team_id" def _sub_select(self): - return super(account_invoice_report, self)._sub_select() + ", ai.team_id as team_id" + return super(AccountInvoiceReport, self)._sub_select() + ", ai.team_id as team_id" def _group_by(self): - return super(account_invoice_report, self)._group_by() + ", ai.team_id" + return super(AccountInvoiceReport, self)._group_by() + ", ai.team_id" diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index 3359e339efd7..5ee8d33c6a98 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -11,33 +11,32 @@ class sale_report(osv.osv): _rec_name = 'date' _columns = { - 'date': fields.datetime('Date Order', readonly=True), # TDE FIXME master: rename into date_order - 'date_confirm': fields.date('Date Confirm', readonly=True), + 'date': fields.datetime('Date Order', readonly=True), 'product_id': fields.many2one('product.product', 'Product', readonly=True), 'product_uom': fields.many2one('product.uom', 'Unit of Measure', readonly=True), 'product_uom_qty': fields.float('# of Qty', readonly=True), - + 'qty_delivered': fields.float('Qty Delivered', readonly=True), + 'qty_to_invoice': fields.float('Qty To Invoice', readonly=True), + 'qty_invoiced': fields.float('Qty Invoiced', readonly=True), 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True), 'company_id': fields.many2one('res.company', 'Company', readonly=True), 'user_id': fields.many2one('res.users', 'Salesperson', readonly=True), 'price_total': fields.float('Total Price', readonly=True), - 'delay': fields.float('Commitment Delay', digits=(16,2), readonly=True), 'product_tmpl_id': fields.many2one('product.template', 'Product Template', readonly=True), 'categ_id': fields.many2one('product.category','Product Category', readonly=True), - 'nbr': fields.integer('# of Lines', readonly=True), # TDE FIXME master: rename into nbr_lines - 'state': fields.selection([ - ('cancel', 'Cancelled'), - ('draft', 'Draft'), - ('confirmed', 'Confirmed'), - ('exception', 'Exception'), - ('done', 'Done')], 'Order Status', readonly=True), + 'nbr': fields.integer('# of Lines', readonly=True), 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True), - 'invoiced': fields.boolean('Paid', readonly=True), - 'nbr_paid': fields.integer('# of Paid Lines', readonly=True), 'team_id': fields.many2one('crm.team', 'Sales Team', readonly=True, oldname='section_id'), 'country_id': fields.many2one('res.country', 'Partner Country', readonly=True), 'commercial_partner_id': fields.many2one('res.partner', 'Commercial Entity', readonly=True), + 'state': fields.selection([ + ('draft', 'Draft Quotation'), + ('sent', 'Quotation Sent'), + ('sale', 'Sales Order'), + ('done', 'Sales Done'), + ('cancel', 'Cancelled'), + ], string='Status', readonly=True), } _order = 'date desc' @@ -56,22 +55,23 @@ class sale_report(osv.osv): l.product_id as product_id, t.uom_id as product_uom, sum(l.product_uom_qty / u.factor * u2.factor) as product_uom_qty, - sum(l.product_uom_qty * cr.rate * l.price_unit * (100.0-l.discount) / 100.0) as price_total, + sum(l.qty_delivered / u.factor * u2.factor) as qty_delivered, + sum(l.qty_invoiced / u.factor * u2.factor) as qty_invoiced, + sum(l.qty_to_invoice / u.factor * u2.factor) as qty_to_invoice, + sum(l.price_total * cr.rate) as price_total, + sum(l.price_subtotal * cr.rate) as price_subtotal, count(*) as nbr, s.date_order as date, - s.date_confirm as date_confirm, + s.state as state, s.partner_id as partner_id, s.user_id as user_id, s.company_id as company_id, - extract(epoch from avg(date_trunc('day',s.date_confirm)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay, - l.state, + extract(epoch from avg(date_trunc('day',s.date_order)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay, t.categ_id as categ_id, s.pricelist_id as pricelist_id, s.project_id as analytic_account_id, s.team_id as team_id, p.product_tmpl_id, - l.invoiced::integer as nbr_paid, - l.invoiced, partner.country_id as country_id, partner.commercial_partner_id as commercial_partner_id """ @@ -100,16 +100,14 @@ class sale_report(osv.osv): t.uom_id, t.categ_id, s.date_order, - s.date_confirm, s.partner_id, s.user_id, + s.state, s.company_id, - l.state, s.pricelist_id, s.project_id, s.team_id, p.product_tmpl_id, - l.invoiced, partner.country_id, partner.commercial_partner_id """ diff --git a/addons/sale/report/sale_report_view.xml b/addons/sale/report/sale_report_view.xml index f05b25e1f3f0..092c37ba451b 100644 --- a/addons/sale/report/sale_report_view.xml +++ b/addons/sale/report/sale_report_view.xml @@ -58,10 +58,9 @@ <field name="arch" type="xml"> <search string="Sales Analysis"> <field name="date"/> - <field name="date_confirm"/> <filter string="This Year" name="year" invisible="1" domain="[('date','<=', time.strftime('%%Y-12-31')),('date','>=',time.strftime('%%Y-01-01'))]"/> <filter name="Quotations" domain="[('state','in',('draft'))]"/> - <filter name="Sales" string="Sales" domain="[('state','not in',('draft', 'cancel'))]"/> + <filter name="Sales" string="Sales" domain="[('state','not in',('draft', 'cancel', 'sent'))]"/> <separator/> <field name="partner_id"/> <field name="product_id"/> diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py index abd574d1938d..d053f38a936a 100644 --- a/addons/sale/res_config.py +++ b/addons/sale/res_config.py @@ -4,7 +4,6 @@ import logging from openerp.osv import fields, osv -from openerp.tools.translate import _ _logger = logging.getLogger(__name__) @@ -19,15 +18,6 @@ class sale_configuration(osv.TransientModel): ], "Product Variants", help='Work with product variant allows you to define some variant of the same products, an ease the product management in the ecommerce for example', implied_group='product.group_product_variant'), - 'module_sale_contract': fields.selection([ - (0, 'Sell based on sales order only'), - (1, 'Activate contract management to track costs and revenues') - ], "Contracts", - help='Allows to define your customer contracts conditions: invoicing ' - 'method (fixed price, on timesheet, advance invoice), the exact pricing ' - '(650€/day for a developer), the duration (one year support contract).\n' - 'You will be able to follow the progress of the contract and invoice automatically.\n' - '-It installs the sale_contract module.'), 'group_sale_pricelist':fields.selection([ (0, 'Set a fixed sale price on each product'), (1, 'Use pricelists to adapt your price per customers or products') @@ -67,10 +57,19 @@ class sale_configuration(osv.TransientModel): 'group_sale_delivery_address': fields.selection([ (0, "Invoicing and shipping addresses are always the same (Example: services companies)"), (1, 'Have 3 fields on sales orders: customer, invoice address, delivery address') - ], "Customer Addresses", - implied_group='sale.group_delivery_invoice_address'), + ], "Customer Addresses", implied_group='sale.group_delivery_invoice_address'), + 'default_invoice_policy': fields.selection([ + ('order', 'Invoice ordered quantities'), + ('delivery', 'Invoice delivered quantities'), + ('cost', 'Invoice based on costs (time and material, expenses)') + ], 'Default Invoicing', default_model='product.template') } + _defaults = { + 'default_invoice_policy': 'order', + } + + def set_sale_defaults(self, cr, uid, ids, context=None): return {} diff --git a/addons/sale/res_config_view.xml b/addons/sale/res_config_view.xml index 097b6aaa8fc8..91a68c6bab8a 100644 --- a/addons/sale/res_config_view.xml +++ b/addons/sale/res_config_view.xml @@ -11,6 +11,7 @@ <field name="group_product_variant" widget="radio"/> <field name="module_website_sale_digital" widget="radio"/> <field name="group_uom" widget="radio"/> + <field name="default_invoice_policy" widget="radio"/> </group> <group string="Quotations & Sales" id="sale"> <field name="group_sale_pricelist" widget="radio"/> @@ -20,9 +21,6 @@ <field name="module_website_quote" widget="radio"/> <field name="module_sale_margin" widget="radio"/> </group> - <group string="Contracts" id="contract"> - <field name="module_sale_contract" widget="radio"/> - </group> </div> </field> </record> diff --git a/addons/sale/res_partner_view.xml b/addons/sale/res_partner_view.xml index 643373953592..a877cf803109 100644 --- a/addons/sale/res_partner_view.xml +++ b/addons/sale/res_partner_view.xml @@ -6,7 +6,7 @@ <field name="res_model">sale.order</field> <field name="view_type">form</field> <field name="view_mode">tree,form,graph</field> - <field name="context">{'search_default_partner_id': active_id}</field> + <field name="context">{'search_default_partner_id': active_id, 'show_sale': True}</field> <field name="groups_id" eval="[(4, ref('base.group_sale_salesman'))]"/> <field name="help" type="html"> <p class="oe_view_nocontent_create"> diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 32ac3d8e1af2..b8a54a6faa26 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -2,561 +2,307 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime, timedelta -import time from openerp import SUPERUSER_ID -from openerp.addons.analytic.models import analytic -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from openerp import api, fields, models, _ import openerp.addons.decimal_precision as dp -from openerp import workflow from openerp.exceptions import UserError +from openerp.tools import float_is_zero, float_compare, DEFAULT_SERVER_DATETIME_FORMAT -class res_company(osv.Model): + +class res_company(models.Model): _inherit = "res.company" - _columns = { - 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."), - } + sale_note = fields.Text(string='Default Terms and Conditions', translate=True) + -class sale_order(osv.osv): +class SaleOrder(models.Model): _name = "sale.order" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Sales Order" - - def _amount_line_tax(self, cr, uid, line, context=None): - val = 0.0 - if line.tax_id.ids: - for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id.ids, line.price_unit * (1-(line.discount or 0.0)/100.0), line.order_id.currency_id.id, line.product_uom_qty, line.product_id.id, line.order_id.partner_id.id)['taxes']: - val += c.get('amount', 0.0) - return val - - def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None): - """ Wrapper because of direct method passing as parameter for function fields """ - return self._amount_all(cr, uid, ids, field_name, arg, context=context) - - def _amount_all(self, cr, uid, ids, field_name, arg, context=None): - cur_obj = self.pool.get('res.currency') - res = {} - for order in self.browse(cr, uid, ids, context=context): - res[order.id] = { - 'amount_untaxed': 0.0, - 'amount_tax': 0.0, - 'amount_total': 0.0, - } - val = val1 = 0.0 - cur = order.pricelist_id.currency_id - for line in order.order_line: - val1 += line.price_subtotal - val += self._amount_line_tax(cr, uid, line, context=context) - res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val) - res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1) - res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax'] - return res - - def _search_invoiced(self, cursor, user, obj, name, args, context=None): - if not len(args): - return [] - clause = '' - sale_clause = '' - no_invoiced = False - for arg in args: - if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]): - clause += 'AND inv.state = \'paid\'' - else: - clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id ' - sale_clause = ', sale_order AS sale ' - no_invoiced = True - - cursor.execute('SELECT rel.order_id ' \ - 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \ - 'WHERE rel.invoice_id = inv.id ' + clause) - res = cursor.fetchall() - if no_invoiced: - cursor.execute('SELECT sale.id ' \ - 'FROM sale_order AS sale ' \ - 'WHERE sale.id NOT IN ' \ - '(SELECT rel.order_id ' \ - 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'') - res.extend(cursor.fetchall()) - if not res: - return [('id', '=', 0)] - return [('id', 'in', [x[0] for x in res])] - - def _get_order(self, cr, uid, ids, context=None): - result = {} - for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context): - result[line.order_id.id] = True - return result.keys() - - def _get_default_company(self, cr, uid, context=None): - company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) - if not company_id: - raise UserError(_('There is no default company for the current user!')) - return company_id - - def _get_invoiced(self, cr, uid, ids, field_name, arg, context=None): - res = {} - for order in self.browse(cr, uid, ids, context=context): - res[order.id] = { - 'invoice_count': len(order.invoice_ids), - 'invoiced': False, - } - if order.state != 'manual' and any(invoice.state == 'paid' for invoice in order.invoice_ids): - res[order.id]['invoiced'] = True - return res - - _columns = { - 'name': fields.char('Order Reference', required=True, copy=False, - readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True), - 'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."), - 'client_order_ref': fields.char('Customer Reference', copy=False), - 'state': fields.selection([ - ('draft', 'Draft Quotation'), - ('sent', 'Quotation Sent'), - ('cancel', 'Cancelled'), - ('waiting_date', 'Waiting Schedule'), - ('progress', 'Sales Order'), - ('manual', 'Sale to Invoice'), - ('shipping_except', 'Shipping Exception'), - ('invoice_except', 'Invoice Exception'), - ('done', 'Done'), - ], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\ - \nThe exception status is automatically set when a cancel operation occurs \ - in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\ - but waiting for the scheduler to run on the order date.", select=True), - 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False), - 'validity_date': fields.date('Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), - 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."), - 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False), - 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'), - 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'), - 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."), - 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."), - 'order_policy': fields.selection([ - ('manual', 'On Demand'), - ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, - help="""This field controls how invoice and delivery operations are synchronized."""), - 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."), - 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True), - 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."), - 'contract_state': fields.related('project_id', 'state', string='Contract Status', type='selection', selection=analytic.ANALYTIC_ACCOUNT_STATE), - - 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True), - 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, copy=False, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."), - 'invoice_count': fields.function(_get_invoiced, type='integer', string='Invoices', multi="counts"), - 'invoiced': fields.function(_get_invoiced, fnct_search=_search_invoiced, type='boolean', string='Paid', multi="counts"), - 'note': fields.text('Terms and conditions'), - 'amount_untaxed': fields.function(_amount_all_wrapper, digits=0, string='Untaxed Amount', - store={ - 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), - 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), - }, - multi='sums', help="The amount without tax.", track_visibility='always'), - 'amount_tax': fields.function(_amount_all_wrapper, digits=0, string='Taxes', - store={ - 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), - 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), - }, - multi='sums', help="The tax amount."), - 'amount_total': fields.function(_amount_all_wrapper, digits=0, string='Total', - store={ - 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), - 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10), - }, - multi='sums', help="The total amount."), - - 'payment_term_id': fields.many2one('account.payment.term', string='Payment Term', oldname='payment_term'), - 'fiscal_position_id': fields.many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position'), - 'company_id': fields.many2one('res.company', 'Company'), - 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id', change_default=True), - 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False), - 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'), - } - - _defaults = { - 'date_order': fields.datetime.now, - 'order_policy': 'manual', - 'company_id': _get_default_company, - 'state': 'draft', - 'user_id': lambda obj, cr, uid, context: uid, - 'name': lambda obj, cr, uid, context: '/', - 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'], - 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'], - 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note, - 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), - } - _sql_constraints = [ - ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), - ] _order = 'date_order desc, id desc' - # Form filling - def unlink(self, cr, uid, ids, context=None): - sale_orders = self.read(cr, uid, ids, ['state'], context=context) - unlink_ids = [] - for s in sale_orders: - if s['state'] in ['draft', 'cancel']: - unlink_ids.append(s['id']) - elif s['state'] == 'sent': - raise UserError(_('In order to delete already sent quotation(s), you must cancel it before!')) - else: - raise UserError(_('In order to delete a confirmed sales order, you must cancel it before!')) + @api.depends('order_line.product_uom_qty', 'order_line.discount', 'order_line.price_unit', 'order_line.tax_id') + def _amount_all(self): + amount_untaxed = amount_tax = 0.0 + for line in self.order_line: + amount_untaxed += line.price_subtotal + amount_tax += line.price_tax + self.update({ + 'amount_untaxed': self.pricelist_id.currency_id.round(amount_untaxed), + 'amount_tax': self.pricelist_id.currency_id.round(amount_tax), + 'amount_total': amount_untaxed + amount_tax, + }) - return osv.osv.unlink(self, cr, uid, unlink_ids, context=context) + @api.depends('state', 'order_line.invoice_status') + def _get_invoiced(self): + for order in self: + invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').ids + + if order.state not in ('sale', 'done'): + invoice_status = 'no' + elif any(line.invoice_status == 'to invoice' for line in order.order_line): + invoice_status = 'to invoice' + elif all(line.invoice_status == 'invoiced' for line in order.order_line): + invoice_status = 'invoiced' + elif all(line.invoice_status in ['invoiced', 'upselling'] for line in order.order_line): + invoice_status = 'upselling' + else: + invoice_status = 'no' - def copy_quotation(self, cr, uid, ids, context=None): - id = self.copy(cr, uid, ids[0], context=context) - view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form') - view_id = view_ref and view_ref[1] or False, - return { - 'type': 'ir.actions.act_window', - 'name': _('Sales Order'), - 'res_model': 'sale.order', - 'res_id': id, - 'view_type': 'form', - 'view_mode': 'form', - 'view_id': view_id, - 'target': 'current', - } + order.update({ + 'invoice_count': len(set(invoice_ids)), + 'invoice_ids': invoice_ids, + 'invoice_status': invoice_status + }) - def _track_subtype(self, cr, uid, ids, init_values, context=None): - record = self.browse(cr, uid, ids[0], context=context) - if 'state' in init_values and record.state in ['manual', 'progress']: + @api.model + def _default_note(self): + return self.env.user.company_id.sale_note + + @api.model + def _get_default_team(self): + default_team_id = self.env['crm.team']._get_default_team_id() + return self.env['crm.team'].browse(default_team_id) + + @api.onchange('fiscal_position_id') + def _compute_tax_id(self): + for order in self: + order.order_line._compute_tax_id() + + name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, index=True, default='New') + origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.") + client_order_ref = fields.Char(string='Customer Reference', copy=False) + + state = fields.Selection([ + ('draft', 'Quotation'), + ('sent', 'Quotation Sent'), + ('sale', 'Sale Order'), + ('done', 'Done'), + ('cancel', 'Cancelled'), + ], string='Status', readonly=True, copy=False, index=True, default='draft') + date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Date.context_today) + validity_date = fields.Date(string='Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}) + create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.") + + user_id = fields.Many2one('res.users', string='Salesperson', states={ + 'draft': [('readonly', False)], + 'sent': [('readonly', False)] + }, index=True, track_visibility='onchange', default=lambda self: self.env.user) + partner_id = fields.Many2one('res.partner', string='Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, index=True, track_visibility='always') + partner_invoice_id = fields.Many2one('res.partner', string='Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order.") + partner_shipping_id = fields.Many2one('res.partner', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order.") + + pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order.") + currency_id = fields.Many2one("res.currency", related='pricelist_id.currency_id', string="Currency", readonly=True, required=True) + project_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order.", copy=False) + + order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) + + invoice_count = fields.Integer(string='# of Invoices', compute='_get_invoiced', store=True, readonly=True) + invoice_ids = fields.Many2many("account.invoice", string='Invoices', compute="_get_invoiced", readonly=True, copy=False) + invoice_status = fields.Selection([ + ('upselling', 'Upselling Opportunity'), + ('invoiced', 'Fully Invoiced'), + ('to invoice', 'To Invoice'), + ('no', 'Nothing to Invoice') + ], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, default='no') + + note = fields.Text('Terms and conditions', default=_default_note) + + amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') + amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all', track_visibility='always') + amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all', track_visibility='always') + + payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', oldname='payment_term') + fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position') + company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order')) + team_id = fields.Many2one('crm.team', 'Sales Team', change_default=True, default=_get_default_team) + procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) + + product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') + + @api.multi + def unlink(self): + for order in self: + if order.state != 'draft': + raise UserError(_('You can only delete draft quotations!')) + return super(SaleOrder, self).unlink() + + @api.multi + def _track_subtype(self, init_values): + self.ensure_one() + if 'state' in init_values and self.state == 'sale': return 'sale.mt_order_confirmed' - elif 'state' in init_values and record.state == 'sent': + elif 'state' in init_values and self.state == 'sent': return 'sale.mt_order_sent' - return super(sale_order, self)._track_subtype(cr, uid, ids, init_values, context=context) - - def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None): - if not pricelist_id: - return {} - pricelist = self.pool['product.pricelist'].browse(cr, uid, pricelist_id, context=context) - return {'value': {'currency_id': pricelist.currency_id.id}} - - def get_salenote(self, cr, uid, ids, partner_id, context=None): - context_lang = context.copy() - if partner_id: - partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang - context_lang.update({'lang': partner_lang}) - return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note - - def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position_id, context=None): - r = {'value': {}} - if not company_id: - company_id = self._get_default_company(cr, uid, context=context) - fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, partner_id, delivery_id, context=context) + return super(SaleOrder, self)._track_subtype(init_values) + + + @api.onchange('partner_shipping_id') + def onchange_partner_shipping_id(self): + fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, self.partner_shipping_id.id) if fiscal_position: - r['value']['fiscal_position_id'] = fiscal_position - return r + self.fiscal_position_id = fiscal_position + return {} - def onchange_partner_id(self, cr, uid, ids, part, context=None): - if not part: - return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term_id': False, 'fiscal_position_id': False}} - - part = self.pool.get('res.partner').browse(cr, uid, part, context=context) - addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'], context=context) - pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False - invoice_part = self.pool.get('res.partner').browse(cr, uid, addr['invoice'], context=context) - payment_term = invoice_part.property_payment_term_id and invoice_part.property_payment_term_id.id or False - dedicated_salesman = part.user_id and part.user_id.id or uid - val = { + @api.multi + @api.onchange('partner_id') + def onchange_partner_id(self): + if not self.partner_id: + self.update({ + 'partner_invoice_id': False, + 'partner_shipping_id': False, + 'payment_term_id': False, + 'fiscal_position_id': False, + }) + return + + addr = self.partner_id.address_get(['delivery', 'invoice']) + values = { + 'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False, + 'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False, 'partner_invoice_id': addr['invoice'], 'partner_shipping_id': addr['delivery'], - 'payment_term_id': payment_term, - 'user_id': dedicated_salesman, } - delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context) - val.update(delivery_onchange['value']) - if pricelist: - val['pricelist_id'] = pricelist - if not self.pool['crm.team']._get_default_team_id(cr, uid, context=context) and part.team_id: - val['team_id'] = part.team_id.id - sale_note = self.get_salenote(cr, uid, ids, part.id, context=context) - if sale_note: val.update({'note': sale_note}) - return {'value': val} - - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - if vals.get('name', '/') == '/': - vals['name'] = self.pool.get('ir.sequence').next_by_code(cr, uid, 'sale.order', context=context) or '/' - if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position_id']): - defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value'] - if not vals.get('fiscal_position_id') and vals.get('partner_shipping_id'): - delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context) - defaults.update(delivery_onchange['value']) - vals = dict(defaults, **vals) - ctx = dict(context or {}, mail_create_nolog=True) - new_id = super(sale_order, self).create(cr, uid, vals, context=ctx) - self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx) - return new_id - - def button_dummy(self, cr, uid, ids, context=None): - return True - # FIXME: deprecated method, overriders should be using _prepare_invoice() instead. - # can be removed after 6.1. - def _inv_get(self, cr, uid, order, context=None): - return {} + if self.partner_id.user_id: + values['user_id'] = self.partner_id.user_id.id + if self.partner_id.team_id: + values['team_id'] = self.partner_id.team_id.id + self.update(values) + + @api.model + def create(self, vals): + if vals.get('name', 'New') == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or 'New' + if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): + partner = self.env['res.partner'].browse(vals.get('partner_id')) + addr = partner.address_get(['delivery', 'invoice']) + vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) + vals['partner_shipping_id'] = vals.setdefault('partner_shipping_id', addr['delivery']) + vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist and partner.property_product_pricelist.id) + result = super(SaleOrder, self).create(vals) + self.message_post(body=_("Quotation created")) + return result - def _prepare_invoice(self, cr, uid, order, lines, context=None): + @api.multi + def _prepare_invoice(self): """Prepare the dict of values to create the new invoice for a sales order. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). - - :param browse_record order: sale.order record to invoice - :param list(int) line: list of invoice line IDs that must be - attached to the invoice - :return: dict of value to create() the invoice """ - if context is None: - context = {} - journal_ids = self.pool.get('account.journal').search(cr, uid, - [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], - limit=1) + self.ensure_one() + journal_ids = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.company_id.id)], limit=1) if not journal_ids: - raise UserError(_('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id)) + raise UserError(_('Please define an accounting sale journal for this company.')) invoice_vals = { - 'name': order.client_order_ref or '', - 'origin': order.name, + 'name': self.client_order_ref or '', + 'origin': self.name, 'type': 'out_invoice', - 'reference': order.client_order_ref or order.name, - 'account_id': order.partner_invoice_id.property_account_receivable_id.id, - 'partner_id': order.partner_invoice_id.id, - 'journal_id': journal_ids[0], - 'invoice_line_ids': [(6, 0, lines)], - 'currency_id': order.pricelist_id.currency_id.id, - 'comment': order.note, - 'payment_term_id': order.payment_term_id.id, - 'fiscal_position_id': order.fiscal_position_id.id or order.partner_invoice_id.property_account_position_id.id, - 'date_invoice': context.get('date_invoice', False), - 'company_id': order.company_id.id, - 'user_id': order.user_id and order.user_id.id or False, - 'team_id' : order.team_id.id + 'reference': self.client_order_ref or self.name, + 'account_id': self.partner_invoice_id.property_account_receivable_id.id, + 'partner_id': self.partner_invoice_id.id, + 'journal_id': journal_ids[0].id, + 'currency_id': self.pricelist_id.currency_id.id, + 'comment': self.note, + 'payment_term_id': self.payment_term_id.id, + 'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id, + 'company_id': self.company_id.id, + 'user_id': self.user_id and self.user_id.id, + 'team_id': self.team_id.id } - - # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1 - invoice_vals.update(self._inv_get(cr, uid, order, context=context)) return invoice_vals - def _make_invoice(self, cr, uid, order, lines, context=None): - inv_obj = self.pool.get('account.invoice') - obj_invoice_line = self.pool.get('account.invoice.line') - if context is None: - context = {} - invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context) - from_line_invoice_ids = [] - for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context): - for invoice_line_id in invoiced_sale_line_id.invoice_lines: - if invoice_line_id.invoice_id.id not in from_line_invoice_ids: - from_line_invoice_ids.append(invoice_line_id.invoice_id.id) - for preinv in order.invoice_ids: - if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids: - for preline in preinv.invoice_line_ids: - inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit}) - lines.append(inv_line_id) - inv = self._prepare_invoice(cr, uid, order, lines, context=context) - inv_id = inv_obj.create(cr, uid, inv, context=context) - invoice = self.pool['account.invoice'].browse(cr, uid, inv_id, context=context) - invoice._onchange_payment_term_date_invoice() - invoice.compute_taxes() - return inv_id - - def print_quotation(self, cr, uid, ids, context=None): - ''' - This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow - ''' - assert len(ids) == 1, 'This option should only be used for a single id at a time' - self.signal_workflow(cr, uid, ids, 'quotation_sent') - return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context) - - def manual_invoice(self, cr, uid, ids, context=None): - """ create invoices for the given sales orders (ids), and open the form - view of one of the newly created invoices - """ - mod_obj = self.pool.get('ir.model.data') - - # create invoices through the sales orders' workflow - inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids) - self.signal_workflow(cr, uid, ids, 'manual_invoice') - inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids) - # determine newly created invoices - new_inv_ids = list(inv_ids1 - inv_ids0) - - res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') - res_id = res and res[1] or False, - - return { - 'name': _('Customer Invoices'), - 'view_type': 'form', - 'view_mode': 'form', - 'view_id': [res_id], - 'res_model': 'account.invoice', - 'context': "{'type':'out_invoice'}", - 'type': 'ir.actions.act_window', - 'target': 'current', - 'res_id': new_inv_ids and new_inv_ids[0] or False, + @api.multi + def print_quotation(self): + self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'}) + return self.env['report'].get_action(self, 'sale.report_saleorder') + + @api.multi + def action_view_invoice(self): + self.ensure_one() + imd = self.env['ir.model.data'] + action = imd.xmlid_to_object('account.action_invoice_tree1') + list_view_id = imd.xmlid_to_res_id('account.invoice_tree') + form_view_id = imd.xmlid_to_res_id('account.invoice_form') + + result = { + 'name': action.name, + 'help': action.help, + 'type': action.type, + 'views': [[list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'kanban'], [False, 'calendar'], [False, 'pivot']], + 'target': action.target, + 'context': action.context, + 'res_model': action.res_model, } - - def action_view_invoice(self, cr, uid, ids, context=None): - ''' - This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show. - ''' - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - - result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1') - id = result and result[1] or False - result = act_obj.read(cr, uid, [id], context=context)[0] - #compute the number of invoices to display - inv_ids = [] - for so in self.browse(cr, uid, ids, context=context): - inv_ids += [invoice.id for invoice in so.invoice_ids] - #choose the view_mode accordingly - if len(inv_ids)>1: - result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]" + if len(self.invoice_ids) > 1: + result['domain'] = "[('id','in',%s)]" % self.invoice_ids.ids + elif len(self.invoice_ids) == 1: + result['views'] = [(form_view_id, 'form')] + result['res_id'] = self.invoice_ids.id else: - res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') - result['views'] = [(res and res[1] or False, 'form')] - result['res_id'] = inv_ids and inv_ids[0] or False + result = {'type': 'ir.actions.act_window_close'} return result - def test_no_product(self, cr, uid, order, context): - for line in order.order_line: - if line.state == 'cancel': - continue - if line.product_id and (line.product_id.type in ['consu', 'product']): - return False - return True - - def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None): - if states is None: - states = ['confirmed', 'done', 'exception'] - res = False + @api.multi + def action_invoice_create(self, grouped=False, final=False): + inv_obj = self.env['account.invoice'] + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') invoices = {} - invoice_ids = [] - invoice = self.pool.get('account.invoice') - obj_sale_order_line = self.pool.get('sale.order.line') - partner_currency = {} - # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the - # last day of the last month as invoice date - if date_invoice: - context = dict(context or {}, date_invoice=date_invoice) - for o in self.browse(cr, uid, ids, context=context): - currency_id = o.pricelist_id.currency_id.id - if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id): - raise UserError(_('You cannot group sales having different currencies for the same partner.')) - - partner_currency[o.partner_id.id] = currency_id - lines = [] - for line in o.order_line: - if line.invoiced: - continue - elif (line.state in states): - lines.append(line.id) - created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines) - if created_lines: - invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines)) - if not invoices: - for o in self.browse(cr, uid, ids, context=context): - for i in o.invoice_ids: - if i.state == 'draft': - return i.id - for val in invoices.values(): - if grouped: - res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context) - invoice_ref = '' - origin_ref = '' - for o, l in val: - invoice_ref += (o.client_order_ref or o.name) + '|' - origin_ref += (o.origin or o.name) + '|' - self.write(cr, uid, [o.id], {'state': 'progress'}) - cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res)) - self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context) - #remove last '|' in invoice_ref - if len(invoice_ref) >= 1: - invoice_ref = invoice_ref[:-1] - if len(origin_ref) >= 1: - origin_ref = origin_ref[:-1] - invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref}) - else: - for order, il in val: - res = self._make_invoice(cr, uid, order, il, context=context) - invoice_ids.append(res) - self.write(cr, uid, [order.id], {'state': 'progress'}) - cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res)) - self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context) - return res - - def action_invoice_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context) - return True - - def action_invoice_end(self, cr, uid, ids, context=None): - for this in self.browse(cr, uid, ids, context=context): - for line in this.order_line: - if line.state == 'exception': - line.write({'state': 'confirmed'}) - if this.state == 'invoice_except': - this.write({'state': 'progress'}) - return True - - def action_cancel(self, cr, uid, ids, context=None): - if context is None: - context = {} - sale_order_line_obj = self.pool.get('sale.order.line') - account_invoice_obj = self.pool.get('account.invoice') - for sale in self.browse(cr, uid, ids, context=context): - for inv in sale.invoice_ids: - if inv.state not in ('draft', 'cancel'): - raise UserError(_('Cannot cancel this sales order!') + ':' + _('First cancel all invoices attached to this sales order.')) - inv.signal_workflow('invoice_cancel') - line_ids = [l.id for l in sale.order_line if l.state != 'cancel'] - sale_order_line_obj.button_cancel(cr, uid, line_ids, context=context) - self.write(cr, uid, ids, {'state': 'cancel'}) - return True - def action_button_confirm(self, cr, uid, ids, context=None): - if not context: - context = {} - assert len(ids) == 1, 'This option should only be used for a single id at a time.' - self.signal_workflow(cr, uid, ids, 'order_confirm') - if context.get('send_email'): - self.force_quotation_send(cr, uid, ids, context=context) - return True - - def action_wait(self, cr, uid, ids, context=None): - context = context or {} - for o in self.browse(cr, uid, ids): - if not any(line.state != 'cancel' for line in o.order_line): - raise UserError(_('You cannot confirm a sales order which has no line.')) - noprod = self.test_no_product(cr, uid, o, context) - if (o.order_policy == 'manual') or noprod: - self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) - else: - self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) - self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line if x.state != 'cancel']) - return True - - def action_quotation_send(self, cr, uid, ids, context=None): + for order in self: + group_key = order.id if grouped else (order.partner_id.id, order.currency_id.id) + for line in order.order_line.sorted(key=lambda l: l.qty_to_invoice < 0): + if float_is_zero(line.qty_to_invoice, precision_digits=precision): + continue + if group_key not in invoices: + inv_data = order._prepare_invoice() + invoice = inv_obj.create(inv_data) + invoices[group_key] = invoice + if line.qty_to_invoice > 0: + line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice) + elif line.qty_to_invoice < 0 and (final or invoices[group_key].amount_untaxed > abs(line.qty_to_invoice * line.price_unit)): + line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice) + + for invoice in invoices.values(): + # If invoice is negative, do a refund invoice instead + if invoice.amount_untaxed < 0: + invoice.type = 'out_refund' + for line in invoice.invoice_line_ids: + line.quantity = -line.quantity + # Necessary to force computation of taxes. In account_invoice, they are triggered + # by onchanges, which are not triggered when doing a create. + invoice.compute_taxes() + + return [inv.id for inv in invoices.values()] + + @api.multi + def action_draft(self): + self.filtered(lambda s: s.state in ['cancel', 'sent']).write({'state': 'draft'}) + + @api.multi + def action_cancel(self): + self.write({'state': 'cancel'}) + + @api.multi + def action_quotation_send(self): ''' This function opens a window to compose an email, with the edi sale template message loaded by default ''' - assert len(ids) == 1, 'This option should only be used for a single id at a time.' - ir_model_data = self.pool.get('ir.model.data') + self.ensure_one() + ir_model_data = self.env['ir.model.data'] try: - template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1] + template_id = ir_model_data.get_object_reference('sale', 'email_template_edi_sale')[1] except ValueError: template_id = False try: - compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1] + compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1] except ValueError: compose_form_id = False ctx = dict() ctx.update({ 'default_model': 'sale.order', - 'default_res_id': ids[0], + 'default_res_id': self.ids[0], 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment', @@ -573,12 +319,12 @@ class sale_order(osv.osv): 'context': ctx, } - def force_quotation_send(self, cr, uid, ids, context=None): - for order_id in ids: - email_act = self.action_quotation_send(cr, uid, [order_id], context=context) + @api.multi + def force_quotation_send(self): + for order in self: + email_act = order.action_quotation_send() if email_act and email_act.get('context'): - composer_obj = self.pool['mail.compose.message'] - composer_values = {} + composer_obj = self.env['mail.compose.message'] email_ctx = email_act['context'] template_values = [ email_ctx.get('default_template_id'), @@ -586,672 +332,442 @@ class sale_order(osv.osv): email_ctx.get('default_model'), email_ctx.get('default_res_id'), ] - composer_values.update(composer_obj.onchange_template_id(cr, uid, None, *template_values, context=context).get('value', {})) + composer_values = composer_obj.onchange_template_id(*template_values).get('value', {}) if not composer_values.get('email_from'): - composer_values['email_from'] = self.browse(cr, uid, order_id, context=context).company_id.email + composer_values['email_from'] = order.company_id.email for key in ['attachment_ids', 'partner_ids']: if composer_values.get(key): composer_values[key] = [(6, 0, composer_values[key])] - composer_id = composer_obj.create(cr, uid, composer_values, context=email_ctx) - composer_obj.send_mail(cr, uid, [composer_id], context=email_ctx) + composer_id = composer_obj.with_context(email_ctx).create(composer_values) + composer_id.with_context(email_ctx).send_mail() return True - def action_done(self, cr, uid, ids, context=None): - for order in self.browse(cr, uid, ids, context=context): - self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], {'state': 'done'}, context=context) - return self.write(cr, uid, ids, {'state': 'done'}, context=context) - - def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): - date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context) - return { - 'name': line.name, - 'origin': order.name, - 'date_planned': date_planned, - 'product_id': line.product_id.id, - 'product_qty': line.product_uom_qty, - 'product_uom': line.product_uom.id, - 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty, - 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id, - 'company_id': order.company_id.id, - 'group_id': group_id, - 'sale_line_id': line.id - } - - def _get_date_planned(self, cr, uid, order, line, start_date, context=None): - date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0) - return date_planned - - def _prepare_procurement_group(self, cr, uid, order, context=None): - return {'name': order.name, 'partner_id': order.partner_shipping_id.id} - - def procurement_needed(self, cr, uid, ids, context=None): - #when sale is installed only, there is no need to create procurements, that's only - #further installed modules (sale_service, sale_stock) that will change this. - sale_line_obj = self.pool.get('sale.order.line') - res = [] - for order in self.browse(cr, uid, ids, context=context): - res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], context=context)) - return any(res) - - def action_ignore_delivery_exception(self, cr, uid, ids, context=None): - for sale_order in self.browse(cr, uid, ids, context=context): - self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_count else 'manual'}, context=context) - return True - - def action_ship_create(self, cr, uid, ids, context=None): - """Create the required procurements to supply sales order lines, also connecting - the procurements to appropriate stock moves in order to bring the goods to the - sales order's requested location. + @api.multi + def action_done(self): + self.write({'state': 'done'}) + + @api.model + def _prepare_procurement_group(self): + return {'name': self.name} + + @api.multi + def action_confirm(self): + for order in self: + order.state = 'sale' + order.order_line._action_procurement_create() + if not order.project_id: + for line in order.order_line: + if line.product_id.invoice_policy == 'cost': + order._create_analytic_account() + break + + @api.multi + def _create_analytic_account(self, prefix=None): + for order in self: + name = order.name + if prefix: + name = prefix + ": " + order.name + analytic = self.env['account.analytic.account'].create({ + 'name': name, + 'code': order.client_order_ref, + 'company_id': order.company_id.id, + 'partner_id': order.partner_id.id + }) + order.project_id = analytic - :return: True - """ - context = context or {} - context['lang'] = self.pool['res.users'].browse(cr, uid, uid).lang - procurement_obj = self.pool.get('procurement.order') - sale_line_obj = self.pool.get('sale.order.line') - for order in self.browse(cr, uid, ids, context=context): - proc_ids = [] - vals = self._prepare_procurement_group(cr, uid, order, context=context) - if not order.procurement_group_id: - group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context) - order.write({'procurement_group_id': group_id}) - - for line in order.order_line: - if line.state == 'cancel': - continue - #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate) - if line.procurement_ids: - #first check them to see if they are in exception or not (one of the related moves is cancelled) - procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']]) - line.refresh() - #run again procurement that are in exception in order to trigger another move - except_proc_ids = [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')] - procurement_obj.reset_to_confirmed(cr, uid, except_proc_ids, context=context) - proc_ids += except_proc_ids - elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context): - if (line.state == 'done') or not line.product_id: - continue - vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context) - ctx = context.copy() - ctx['procurement_autorun_defer'] = True - proc_id = procurement_obj.create(cr, uid, vals, context=ctx) - proc_ids.append(proc_id) - #Confirm procurement order such that rules will be applied on it - #note that the workflow normally ensure proc_ids isn't an empty list - procurement_obj.run(cr, uid, proc_ids, context=context) - - #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO - if order.state == 'shipping_except': - val = {'state': 'progress', 'shipped': False} - - if (order.order_policy == 'manual'): - for line in order.order_line: - if (not line.invoiced) and (line.state not in ('cancel', 'draft')): - val['state'] = 'manual' - break - order.write(val) - return True +class SaleOrderLine(models.Model): + _name = 'sale.order.line' + _description = 'Sales Order Line' + _order = 'order_id desc, sequence, id' + @api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced') + def _compute_invoice_status(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for line in self: + if line.state not in ('sale', 'done'): + line.invoice_status = 'no' + elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): + line.invoice_status = 'to invoice' + elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) == 1 or\ + float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0 and\ + float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1: + line.invoice_status = 'upselling' + elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) == 0: + line.invoice_status = 'invoiced' + else: + line.invoice_status = 'no' - def onchange_fiscal_position(self, cr, uid, ids, fiscal_position_id, order_lines, context=None): - '''Update taxes of order lines for each line where a product is defined + @api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id') + def _compute_amount(self): + for line in self: + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id) + line.update({ + 'price_tax': taxes['total_included'] - taxes['total_excluded'], + 'price_total': taxes['total_included'], + 'price_subtotal': taxes['total_excluded'], + }) - :param list ids: not used - :param int fiscal_position_id: sale order fiscal position - :param list order_lines: command list for one2many write method - ''' - order_line = [] - fiscal_obj = self.pool.get('account.fiscal.position') - product_obj = self.pool.get('product.product') - line_obj = self.pool.get('sale.order.line') - - fpos = False - if fiscal_position_id: - fpos = fiscal_obj.browse(cr, uid, fiscal_position_id, context=context) - - for line in order_lines: - # create (0, 0, { fields }) - # update (1, ID, { fields }) - if line[0] in [0, 1]: - prod = None - if line[2].get('product_id'): - prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context) - elif line[1]: - prod = line_obj.browse(cr, uid, line[1], context=context).product_id - if prod and prod.taxes_id: - line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]] - order_line.append(line) - - # link (4, ID) - # link all (6, 0, IDS) - elif line[0] in [4, 6]: - line_ids = line[0] == 4 and [line[1]] or line[2] - for line_id in line_ids: - prod = line_obj.browse(cr, uid, line_id, context=context).product_id - if prod and prod.taxes_id: - order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}]) - else: - order_line.append([4, line_id]) + @api.depends('product_id.invoice_policy', 'order_id.state') + def _compute_qty_delivered_updateable(self): + for line in self: + line.qty_delivered_updateable = line.product_id.invoice_policy in ('order', 'delivery') and line.order_id.state == 'sale' + + @api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state') + def _get_to_invoice_qty(self): + for line in self: + if line.order_id.state in ['sale', 'done']: + if line.product_id.invoice_policy == 'order': + line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced + else: + line.qty_to_invoice = line.qty_delivered - line.qty_invoiced else: - order_line.append(line) - return {'value': {'order_line': order_line, 'amount_untaxed': False, 'amount_tax': False, 'amount_total': False}} + line.qty_to_invoice = 0 + + @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity') + def _get_invoice_qty(self): + for line in self: + qty_invoiced = 0.0 + for invoice_line in line.invoice_lines: + if invoice_line.invoice_id.state != 'cancel': + if invoice_line.invoice_id.type == 'out_invoice': + qty_invoiced += invoice_line.quantity + elif invoice_line.invoice_id.type == 'out_refund': + qty_invoiced -= invoice_line.quantity + line.qty_invoiced = qty_invoiced + + @api.depends('price_subtotal', 'product_uom_qty') + def _get_price_reduce(self): + for line in self: + line.price_reduce = line.price_subtotal / line.product_uom_qty if line.product_uom_qty else 0.0 + + @api.onchange('order_id', 'product_id') + def _compute_tax_id(self): + for line in self: + fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id + if fpos: + # The superuser is used by website_sale in order to create a sale order. We need to make + # sure we only select the taxes related to the company of the partner. This should only + # apply if the partner is linked to a company. + if self.env.uid == SUPERUSER_ID and line.order_id.company_id: + taxes = fpos.map_tax(line.product_id.taxes_id).filtered(lambda r: r.company_id == line.order_id.company_id) + else: + taxes = fpos.map_tax(line.product_id.taxes_id) + line.tax_id = taxes + else: + line.tax_id = False - def test_procurements_done(self, cr, uid, ids, context=None): - for sale in self.browse(cr, uid, ids, context=context): - for line in sale.order_line: - if line.state == 'cancel': - continue - if not all([x.state == 'done' for x in line.procurement_ids]): - return False - return True + @api.multi + def _prepare_order_line_procurement(self, group_id=False): + self.ensure_one() + return { + 'name': self.name, + 'origin': self.order_id.name, + 'date_planned': datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=self.customer_lead), + 'product_id': self.product_id.id, + 'product_qty': self.product_uom_qty, + 'product_uom': self.product_uom.id, + 'company_id': self.order_id.company_id.id, + 'group_id': group_id, + 'sale_line_id': self.id + } - def test_procurements_except(self, cr, uid, ids, context=None): - for sale in self.browse(cr, uid, ids, context=context): - for line in sale.order_line: - if line.state == 'cancel': - continue - if any([x.state == 'cancel' for x in line.procurement_ids]): - return True - return False - - -# TODO add a field price_unit_uos -# - update it on change product and unit price -# - use it in report if there is a uos -class sale_order_line(osv.osv): - - def need_procurement(self, cr, uid, ids, context=None): - #when sale is installed only, there is no need to create procurements, that's only - #further installed modules (sale_service, sale_stock) that will change this. - prod_obj = self.pool.get('product.product') - for line in self.browse(cr, uid, ids, context=context): - if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context): - return True - return False - - def _amount_line(self, cr, uid, ids, field_name, arg, context=None): - tax_obj = self.pool.get('account.tax') - cur_obj = self.pool.get('res.currency') - res = {} - if context is None: - context = {} - for line in self.browse(cr, uid, ids, context=context): - price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - cur_id = line.order_id.pricelist_id.currency_id.id - if line.tax_id.ids: - res[line.id] = tax_obj.compute_all(cr, uid, line.tax_id.ids, price, cur_id, line.product_uom_qty, line.product_id.id, line.order_id.partner_id.id)['total_excluded'] - else: - res[line.id] = price * line.product_uom_qty - return res + @api.multi + def _action_procurement_create(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for line in self: + if line.state != 'sale': + continue + qty = 0.0 + for proc in line.procurement_ids: + qty += proc.product_qty + if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: + return False - def _get_uom_id(self, cr, uid, *args): - try: - proxy = self.pool.get('ir.model.data') - result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit') - return result[1] - except Exception, ex: - return False - - def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None): - res = dict.fromkeys(ids, False) - for this in self.browse(cr, uid, ids, context=context): - res[this.id] = this.invoice_lines and \ - all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) - return res + if not line.order_id.procurement_group_id: + vals = line.order_id._prepare_procurement_group() + line.order_id.procurement_group_id = self.env["procurement.group"].create(vals) - def _order_lines_from_invoice(self, cr, uid, ids, context=None): - # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise) - cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN - sale_order_line sol ON (sol.order_id = rel.order_id) - WHERE rel.invoice_id = ANY(%s)""", (list(ids),)) - return [i[0] for i in cr.fetchall()] - - def _get_price_reduce(self, cr, uid, ids, field_name, arg, context=None): - res = dict.fromkeys(ids, 0.0) - for line in self.browse(cr, uid, ids, context=context): - res[line.id] = line.price_subtotal / line.product_uom_qty - return res + vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id) + vals['product_qty'] = line.product_uom_qty - qty + new_proc = self.env["procurement.order"].create(vals) + new_proc.run() + return True - _name = 'sale.order.line' - _description = 'Sales Order Line' - _columns = { - 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}), - 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."), - 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'), - 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False), - 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean', - store={ - 'account.invoice': (_order_lines_from_invoice, ['state'], 10), - 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10) - }), - 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}), - 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0), - 'price_reduce': fields.function(_get_price_reduce, type='float', string='Price Reduce', digits_compute=dp.get_precision('Product Price')), - 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}), - 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}), - 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}), - 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}), - 'product_uos': fields.many2one('product.uom', 'Product UoS'), - 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}), - 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}), - 'state': fields.selection( - [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], - 'Status', required=True, readonly=True, copy=False, - help='* The \'Draft\' status is set when the related sales order in draft status. \ - \n* The \'Confirmed\' status is set when the related sales order is confirmed. \ - \n* The \'Exception\' status is set when the related sales order is set as exception. \ - \n* The \'Done\' status is set when the sales order line has been picked. \ - \n* The \'Cancelled\' status is set when a user cancel the sales order related.'), - 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'), - 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'), - 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), - 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}), - 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'), - } - _order = 'order_id desc, sequence, id' - _defaults = { - 'product_uom' : _get_uom_id, - 'discount': 0.0, - 'product_uom_qty': 1, - 'product_uos_qty': 1, - 'sequence': 10, - 'state': 'draft', - 'price_unit': 0.0, - 'delay': 0.0, - } - - - - def _get_line_qty(self, cr, uid, line, context=None): - if line.product_uos: - return line.product_uos_qty or 0.0 - return line.product_uom_qty - - def _get_line_uom(self, cr, uid, line, context=None): - if line.product_uos: - return line.product_uos.id - return line.product_uom.id - - def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None): + # Create new procurements if quantities purchased changes + @api.model + def create(self, values): + line = super(SaleOrderLine, self).create(values) + if line.state == 'sale': + line._action_procurement_create() + return line + + # Create new procurements if quantities purchased changes + @api.multi + def write(self, values): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + lines = False + if 'product_uom_qty' in values: + lines = self.filtered( + lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) + lines._action_procurement_create() + return super(SaleOrderLine, self).write(values) + + order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False) + name = fields.Text(string='Description', required=True) + sequence = fields.Integer(string='Sequence', default=10) + + invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False) + invoice_status = fields.Selection([ + ('upselling', 'Upselling Opportunity'), + ('invoiced', 'Fully Invoiced'), + ('to invoice', 'To Invoice'), + ('no', 'Nothing to Invoice') + ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no') + price_unit = fields.Float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price'), default=0.0) + + price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True) + price_tax = fields.Monetary(compute='_compute_amount', string='Taxes', readonly=True, store=True) + price_total = fields.Monetary(compute='_compute_amount', string='Total', readonly=True, store=True) + + price_reduce = fields.Monetary(compute='_get_price_reduce', string='Price Reduce', readonly=True, store=True) + tax_id = fields.Many2many('account.tax', string='Taxes', readonly=True, states={'draft': [('readonly', False)]}) + + discount = fields.Float(string='Discount (%)', digits_compute=dp.get_precision('Discount'), default=0.0) + + product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict', required=True) + product_uom_qty = fields.Float(string='Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, default=1.0) + product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True) + + qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True) + qty_delivered = fields.Float(string='Delivered', copy=False, digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0) + qty_to_invoice = fields.Float( + compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True, + digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0) + qty_invoiced = fields.Float( + compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True, + digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0) + + salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', readonly=True) + currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) + company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, readonly=True) + order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer') + + state = fields.Selection([ + ('draft', 'Quotation'), + ('sent', 'Quotation Sent'), + ('sale', 'Sale Order'), + ('done', 'Done'), + ('cancel', 'Cancelled'), + ], related='order_id.state', string='Order Status', readonly=True, copy=False, store=True, default='draft') + + customer_lead = fields.Float( + 'Delivery Lead Time', required=True, default=0.0, + help="Number of days between the order confirmation and the shipping of the products to the customer", oldname="delay") + procurement_ids = fields.One2many('procurement.order', 'sale_line_id', string='Procurements') + + @api.multi + def _prepare_invoice_line(self, qty): """Prepare the dict of values to create the new invoice line for a sales order line. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). - :param browse_record line: sale.order.line record to invoice - :param int account_id: optional ID of a G/L account to force - (this is used for returning products including service) - :return: dict of values to create() the invoice line + :param qty : float quantity to invoice """ + self.ensure_one() res = {} - if not line.invoiced: - if not account_id: - if line.product_id: - account_id = line.product_id.property_account_income_id.id - if not account_id: - account_id = line.product_id.categ_id.property_account_income_categ_id.id - if not account_id: - raise UserError( - _('Please define income account for this product: "%s" (id:%d).') % \ - (line.product_id.name, line.product_id.id,)) - else: - prop = self.pool.get('ir.property').get(cr, uid, - 'property_account_income_categ_id', 'product.category', - context=context) - account_id = prop and prop.id or False - uosqty = self._get_line_qty(cr, uid, line, context=context) - uos_id = self._get_line_uom(cr, uid, line, context=context) - pu = 0.0 - if uosqty: - pu = round(line.price_unit * line.product_uom_qty / uosqty, - self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price')) - fpos = line.order_id.fiscal_position_id or False - account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id) - if not account_id: - raise UserError(_('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.')) - res = { - 'name': line.name, - 'sequence': line.sequence, - 'origin': line.order_id.name, - 'account_id': account_id, - 'price_unit': pu, - 'quantity': uosqty, - 'discount': line.discount, - 'uos_id': uos_id, - 'product_id': line.product_id.id or False, - 'invoice_line_tax_ids': [(6, 0, [x.id for x in line.tax_id])], - 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False, - } - + account_id = self.product_id.property_account_income_id.id or self.product_id.categ_id.property_account_income_categ_id.id + if not account_id: + raise UserError(_('Please define income account for this product: "%s" (id:%d) - or for its category: "%s".') % \ + (self.product_id.name, self.product_id.id, self.product_id.categ_id.name)) + + fpos = self.order_id.fiscal_position_id or self.order_id.partner_id.property_account_position_id + if fpos: + account_id = self.order_id.fiscal_position_id.map_account(account_id) + + res = { + 'name': self.name, + 'sequence': self.sequence, + 'origin': self.order_id.name, + 'account_id': account_id, + 'price_unit': self.price_unit, + 'quantity': qty, + 'discount': self.discount, + 'uom_id': self.product_uom.id, + 'product_id': self.product_id.id or False, + 'invoice_line_tax_ids': [(6, 0, self.tax_id.ids)], + 'account_analytic_id': self.order_id.project_id.id, + } return res - def invoice_line_create(self, cr, uid, ids, context=None): - if context is None: - context = {} - - create_ids = [] - sales = set() - for line in self.browse(cr, uid, ids, context=context): - vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context) - if vals: - inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context) - self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context) - sales.add(line.order_id.id) - create_ids.append(inv_id) - # Trigger workflow events - for sale_id in sales: - workflow.trg_write(uid, 'sale.order', sale_id, cr) - return create_ids - - def button_cancel(self, cr, uid, ids, context=None): - lines = self.browse(cr, uid, ids, context=context) - for line in lines: - if line.invoiced: - raise UserError(_('You cannot cancel a sales order line that has already been invoiced.')) - procurement_obj = self.pool['procurement.order'] - procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in lines], []), context=context) - return self.write(cr, uid, ids, {'state': 'cancel'}) - - def button_confirm(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'confirmed'}) - - def button_done(self, cr, uid, ids, context=None): - res = self.write(cr, uid, ids, {'state': 'done'}) - for line in self.browse(cr, uid, ids, context=context): - workflow.trg_write(uid, 'sale.order', line.order_id.id, cr) - return res + @api.multi + def invoice_line_create(self, invoice_id, qty): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for line in self: + if not float_is_zero(qty, precision_digits=precision): + vals = line._prepare_invoice_line(qty=qty) + vals.update({'invoice_id': invoice_id, 'sale_line_ids': [(6, 0, [line.id])]}) + self.env['account.invoice.line'].create(vals) + + @api.multi + @api.onchange('product_id') + def product_id_change(self): + if not self.product_id: + return {'domain': {'product_uom': []}} + + vals = {} + domain = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]} + if not (self.product_uom and (self.product_id.uom_id.category_id.id == self.product_uom.category_id.id)): + vals['product_uom'] = self.product_id.uom_id + + product = self.product_id.with_context( + lang=self.order_id.partner_id.lang, + partner=self.order_id.partner_id.id, + quantity=self.product_uom_qty, + date=self.order_id.date_order, + pricelist=self.order_id.pricelist_id.id, + uom=self.product_uom.id + ) + + name = product.name_get()[0][1] + if product.description_sale: + name += '\n' + product.description_sale + vals['name'] = name + + if self.order_id.pricelist_id and self.order_id.partner_id: + vals['price_unit'] = product.price + self.update(vals) + return {'domain': domain} + + @api.onchange('product_uom') + def product_uom_change(self): + if not self.product_uom: + self.price_unit = 0.0 + return + if self.order_id.pricelist_id and self.order_id.partner_id: + product = self.product_id.with_context( + lang=self.order_id.partner_id.lang, + partner=self.order_id.partner_id.id, + quantity=self.product_uom_qty, + date_order=self.order_id.date_order, + pricelist=self.order_id.pricelist_id.id, + uom=self.product_uom.id + ) + self.price_unit = product.price - def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None): - product_obj = self.pool.get('product.product') - if not product_id: - return {'value': {'product_uom': product_uos, - 'product_uom_qty': product_uos_qty}, 'domain': {}} + @api.multi + def unlink(self): + if self.filtered(lambda x: x.state in ('sale', 'done')): + raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.')) + return super(SaleOrderLine, self).unlink() - product = product_obj.browse(cr, uid, product_id) - value = { - 'product_uom': product.uom_id.id, - } - # FIXME must depend on uos/uom of the product and not only of the coeff. - try: - value.update({ - 'product_uom_qty': product_uos_qty / product.uos_coeff, - 'th_weight': product_uos_qty / product.uos_coeff * product.weight - }) - except ZeroDivisionError: - pass - return {'value': value} - - def create(self, cr, uid, values, context=None): - if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']): - order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position_id'], context=context) - defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'], - qty=float(values.get('product_uom_qty', False)), - uom=values.get('product_uom', False), - qty_uos=float(values.get('product_uos_qty', False)), - uos=values.get('product_uos', False), - name=values.get('name', False), - partner_id=order['partner_id'][0], - date_order=order['date_order'], - fiscal_position_id=order['fiscal_position_id'][0] if order['fiscal_position_id'] else False, - flag=False, # Force name update - context=context - )['value'] - if defaults.get('tax_id'): - defaults['tax_id'] = [[6, 0, defaults['tax_id']]] - values = dict(defaults, **values) - return super(sale_order_line, self).create(cr, uid, values, context=context) - - def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, - uom=False, qty_uos=0, uos=False, name='', partner_id=False, - lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position_id=False, flag=False, context=None): - if context is None: - context = {} - Partner = self.pool['res.partner'] - ProductUom = self.pool['product.uom'] - Product = self.pool['product.product'] - ctx_product = dict(context) - partner = False - if partner_id: - partner = Partner.browse(cr, uid, partner_id, context=context) - ctx_product['lang'] = partner.lang - ctx_product['partner_id'] = partner_id - elif lang: - ctx_product['lang'] = lang - - if not product: - return {'value': {'th_weight': 0, - 'product_uos_qty': qty}, 'domain': {'product_uom': [], - 'product_uos': []}} - if not date_order: - date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT) - - result = {} - product_obj = Product.browse(cr, uid, product, context=ctx_product) - - uom2 = False - if uom: - uom2 = ProductUom.browse(cr, uid, uom, context=context) - if product_obj.uom_id.category_id.id != uom2.category_id.id: - uom = False - if uos: - if product_obj.uos_id: - uos2 = ProductUom.browse(cr, uid, uos, context=context) - if product_obj.uos_id.category_id.id != uos2.category_id.id: - uos = False - else: - uos = False + @api.multi + def _get_delivered_qty(self): + ''' + Intended to be overridden in sale_stock and sale_mrp + :return: the quantity delivered + :rtype: float + ''' + return 0.0 - fpos = False - if not fiscal_position_id: - fpos = partner and partner.property_account_position_id or False - else: - fpos = self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position_id) - if update_tax: # The quantity only have changed - # The superuser is used by website_sale in order to create a sale order. We need to make - # sure we only select the taxes related to the company of the partner. This should only - # apply if the partner is linked to a company. - if uid == SUPERUSER_ID and partner.company_id: - taxes = product_obj.taxes_id.filtered(lambda r: r.company_id == partner.company_id) - else: - taxes = product_obj.taxes_id - result['tax_id'] = self.pool['account.fiscal.position'].map_tax(cr, uid, fpos, taxes) - - if not flag: - result['name'] = Product.name_get(cr, uid, [product_obj.id], context=ctx_product)[0][1] - if product_obj.description_sale: - result['name'] += '\n'+product_obj.description_sale - domain = {} - if (not uom) and (not uos): - result['product_uom'] = product_obj.uom_id.id - if product_obj.uos_id: - result['product_uos'] = product_obj.uos_id.id - result['product_uos_qty'] = qty * product_obj.uos_coeff - uos_category_id = product_obj.uos_id.category_id.id - else: - result['product_uos'] = False - result['product_uos_qty'] = qty - uos_category_id = False - result['th_weight'] = qty * product_obj.weight - domain = {'product_uom': - [('category_id', '=', product_obj.uom_id.category_id.id)], - 'product_uos': - [('category_id', '=', uos_category_id)]} - elif uos and not uom: # only happens if uom is False - result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id - result['product_uom_qty'] = qty_uos / product_obj.uos_coeff - result['th_weight'] = result['product_uom_qty'] * product_obj.weight - elif uom: # whether uos is set or not - default_uom = product_obj.uom_id and product_obj.uom_id.id - q = ProductUom._compute_qty(cr, uid, uom, qty, default_uom) - if product_obj.uos_id: - result['product_uos'] = product_obj.uos_id.id - result['product_uos_qty'] = qty * product_obj.uos_coeff - else: - result['product_uos'] = False - result['product_uos_qty'] = qty - result['th_weight'] = q * product_obj.weight # Round the quantity up - - if not uom2: - uom2 = product_obj.uom_id - - if pricelist and partner_id: - ctx = dict( - context, - uom=uom or result.get('product_uom'), - date=date_order, - ) - price = self.pool['product.pricelist'].price_get(cr, uid, [pricelist], - product, qty or 1.0, partner_id, ctx)[pricelist] - else: - price = Product.price_get(cr, uid, [product], ptype='list_price', context=ctx_product)[product] or False - if context.get('uom_qty_change', False): - result, domain = {}, {} - result.update({'price_unit': price}) - return {'value': result, 'domain': domain} - - def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0, - uom=False, qty_uos=0, uos=False, name='', partner_id=False, - lang=False, update_tax=True, date_order=False, context=None): - context = context or {} - lang = lang or ('lang' in context and context['lang']) - if not uom: - return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}} - return self.product_id_change(cursor, user, ids, pricelist, product, - qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, - partner_id=partner_id, lang=lang, update_tax=update_tax, - date_order=date_order, context=context) - - def unlink(self, cr, uid, ids, context=None): - if context is None: - context = {} - """Allows to delete sales order lines in draft,cancel states""" - for rec in self.browse(cr, uid, ids, context=context): - if rec.state not in ['draft', 'cancel']: - raise UserError(_('Cannot delete a sales order line which is in state \'%s\'.') % (rec.state,)) - return super(sale_order_line, self).unlink(cr, uid, ids, context=context) - - -class mail_compose_message(osv.Model): + +class MailComposeMessage(models.TransientModel): _inherit = 'mail.compose.message' - def send_mail(self, cr, uid, ids, auto_commit=False, context=None): - context = context or {} - if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'): - context = dict(context, mail_post_autofollow=True) - self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent') - return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context) + @api.multi + def send_mail(self, auto_commit=False): + if self._context.get('default_model') == 'sale.order' and self._context.get('default_res_id') and self._context.get('mark_so_as_sent'): + order = self.env['sale.order'].browse([self._context['default_res_id']]) + if order.state == 'draft': + order.state = 'sent' + return super(MailComposeMessage, self.with_context(mail_post_autofollow=True)).send_mail() -class account_invoice(osv.Model): +class AccountInvoice(models.Model): _inherit = 'account.invoice' - _columns = { - 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'), - 'sale_ids': fields.many2many('sale.order', 'sale_order_invoice_rel', 'invoice_id', 'order_id', 'Sale Orders', - readonly=True, copy=False, help="This is the list of sale orders related to this invoice. One invoice may have multiple sale orders related. "), - } - - _defaults = { - 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), - } - - def confirm_paid(self, cr, uid, ids, context=None): - sale_order_obj = self.pool.get('sale.order') - res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context) - so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context) - for so_id in so_ids: - sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context) + @api.model + def _get_default_team(self): + default_team_id = self.env['crm.team']._get_default_team_id() + return self.env['crm.team'].browse(default_team_id) + + team_id = fields.Many2one('crm.team', string='Sales Team', default=_get_default_team) + + @api.multi + def confirm_paid(self): + res = super(AccountInvoice, self).confirm_paid() + todo = set() + for invoice in self: + for line in invoice.invoice_line_ids: + for sale_line in line.sale_line_ids: + todo.add((sale_line.order_id, invoice.number)) + for (order, name) in todo: + order.message_post(body=_("Invoice %s paid") % (name)) return res - def unlink(self, cr, uid, ids, context=None): - """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """ - invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context) - #if we can't cancel all invoices, do nothing - if len(invoice_ids) == len(ids): - #Cancel invoice(s) first before deleting them so that if any sale order is associated with them - #it will trigger the workflow to put the sale order in an 'invoice exception' state - for id in ids: - workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr) - return super(account_invoice, self).unlink(cr, uid, ids, context=context) - -class account_invoice_line(osv.Model): +class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' - - _columns= { - 'sale_line_ids': fields.many2many('sale.order.line', 'sale_order_line_invoice_rel', 'invoice_id', 'order_line_id', - 'Sale Order Lines', readonly=True, copy=False) - } + sale_line_ids = fields.Many2many('sale.order.line', 'sale_order_line_invoice_rel', 'invoice_line_id', 'order_line_id', string='Sale Order Lines', readonly=True, copy=False) -class procurement_order(osv.osv): +class ProcurementOrder(models.Model): _inherit = 'procurement.order' - _columns = { - 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'), - } - - def write(self, cr, uid, ids, vals, context=None): - if isinstance(ids, (int, long)): - ids = [ids] - res = super(procurement_order, self).write(cr, uid, ids, vals, context=context) - from openerp import workflow - if vals.get('state') in ['done', 'cancel', 'exception']: - for proc in self.browse(cr, uid, ids, context=context): - if proc.sale_line_id and proc.sale_line_id.order_id: - order_id = proc.sale_line_id.order_id.id - if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context): - workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr) - if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context): - workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr) - return res + sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line') + -class product_product(osv.Model): +class ProductProduct(models.Model): _inherit = 'product.product' - def _sales_count(self, cr, uid, ids, field_name, arg, context=None): - r = dict.fromkeys(ids, 0) + @api.multi + def _sales_count(self): + r = {} domain = [ - ('state', 'in', ['confirmed', 'done']), - ('product_id', 'in', ids), + ('state', 'in', ['sale', 'done']), + ('product_id', 'in', self.ids), ] - for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id', 'product_uom_qty'], ['product_id'], context=context): + for group in self.env['sale.report'].read_group(domain, ['product_id', 'product_uom_qty'], ['product_id']): r[group['product_id'][0]] = group['product_uom_qty'] + for product in self: + product.sales_count = r.get(product.id, 0) return r - def action_view_sales(self, cr, uid, ids, context=None): - result = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree', raise_if_not_found=True) - result = self.pool['ir.actions.act_window'].read(cr, uid, [result], context=context)[0] - result['domain'] = "[('product_id','in',[" + ','.join(map(str, ids)) + "])]" - return result + sales_count = fields.Integer(compute='_sales_count', string='# Sales') - _columns = { - 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'), - } -class product_template(osv.Model): +class ProductTemplate(models.Model): _inherit = 'product.template' + track_service = fields.Selection([('manual', 'Manually set quantities on order')], string='Track Service', default='manual') - def _sales_count(self, cr, uid, ids, field_name, arg, context=None): - res = dict.fromkeys(ids, 0) - for template in self.browse(cr, uid, ids, context=context): - res[template.id] = sum([p.sales_count for p in template.product_variant_ids]) - return res - - def action_view_sales(self, cr, uid, ids, context=None): - act_obj = self.pool.get('ir.actions.act_window') - mod_obj = self.pool.get('ir.model.data') - product_ids = [] - for template in self.browse(cr, uid, ids, context=context): - product_ids += [x.id for x in template.product_variant_ids] - result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True) - result = act_obj.read(cr, uid, [result], context=context)[0] - result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]" - return result + @api.multi + @api.depends('product_variant_ids.sales_count') + def _sales_count(self): + for product in self: + product.sales_count = sum([p.sales_count for p in product.product_variant_ids]) + @api.multi + def action_view_sales(self): + self.ensure_one() + action = self.env.ref('sale.action_product_sale_list') + product_ids = self.product_variant_ids.ids - _columns = { - 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'), + return { + 'name': action.name, + 'help': action.help, + 'type': action.type, + 'view_type': action.view_type, + 'view_mode': action.view_mode, + 'target': action.target, + 'context': "{'search_default_product_id': " + ','.join(map(str, product_ids)) + ", 'default_product_id': " + str(product_ids[0]) + "}", + 'res_model': action.res_model, + 'domain': action.domain, + } - } + sales_count = fields.Integer(compute='_sales_count', string='# Sales') + invoice_policy = fields.Selection( + [('order', 'Ordered quantities'), + ('delivery', 'Delivered quantities'), + ('cost', 'Invoice at cost (time and material, expenses)')], + string='Invoicing Policy', default='order') diff --git a/addons/sale/sale_analytic.py b/addons/sale/sale_analytic.py new file mode 100644 index 000000000000..1910320ca8fe --- /dev/null +++ b/addons/sale/sale_analytic.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + @api.multi + def _compute_analytic(self, domain=None): + lines = {} + if not domain: + domain = [('so_line', 'in', self.ids), ('amount', '<', 0.0)] + data = self.env['account.analytic.line'].read_group( + domain, + ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False + ) + for d in data: + if not d['product_uom_id']: + continue + line = self.browse(d['so_line'][0]) + lines.setdefault(line, 0.0) + uom = self.env['product.uom'].browse(d['product_uom_id'][0]) + + qty = self.env['product.uom']._compute_qty_obj(uom, d['unit_amount'], line.product_uom) + lines[line] += qty + + for line, qty in lines.items(): + line.qty_delivered = qty + return True + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + so_line = fields.Many2one('sale.order.line', string='Sale Order Line') + + def _get_invoice_price(self, order): + return abs(self.amount / self.unit_amount) + + def _get_sale_order_line_vals(self): + order = self.env['sale.order'].search([('project_id', '=', self.account_id.id), ('state', '=', 'sale')], limit=1) + + 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.partner_id.property_account_position_id + taxes = fpos.map_tax(self.product_id.taxes_id) + price = self._get_invoice_price(order) + + 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, + 'qty_delivered': self.unit_amount, + } + + def _get_sale_order_line(self, vals=None): + result = dict(vals or {}) + sol = result.get('so_line', False) or self.so_line + if not sol and self.account_id and self.product_id and self.product_id.invoice_policy in ('cost', 'order'): + sol = self.env['sale.order.line'].search([ + ('order_id.project_id', '=', self.account_id.id), + ('state', '=', 'sale'), + ('product_id', '=', self.product_id.id)], + limit=1) + result.update({'so_line': sol.id}) + + if not sol and self.account_id and self.product_id and self.product_id.invoice_policy == 'cost': + order_line_vals = self._get_sale_order_line_vals() + sol = self.env['sale.order.line'].create(order_line_vals) + sol._compute_tax_id() + result.update({'so_line': sol.id}) + + return result + + @api.multi + def write(self, values): + if self._context.get('create', False): + return super(AccountAnalyticLine, self).write(values) + + todo = self.mapped('so_line') + result = super(AccountAnalyticLine, self).write(values) + if 'so_line' in values: + todo |= self.mapped('so_line') + + for line in self: + res = self._get_sale_order_line(vals=values) + super(AccountAnalyticLine, line).write(res) + if 'so_line' in res: + todo |= line.mapped('so_line') + + todo._compute_analytic() + return result + + @api.model + def create(self, values): + line = super(AccountAnalyticLine, self).create(values) + res = line._get_sale_order_line(vals=values) + line.with_context(create=True).write(res) + line.mapped('so_line')._compute_analytic() + return line diff --git a/addons/sale/sale_demo.xml b/addons/sale/sale_demo.xml index 9857cfe6ca04..2fabc306fcb3 100644 --- a/addons/sale/sale_demo.xml +++ b/addons/sale/sale_demo.xml @@ -25,7 +25,6 @@ <field name="name">Laptop E5023</field> <field name="product_id" ref="product.product_product_25"/> <field name="product_uom_qty">3</field> - <field name="product_uos_qty">3</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">2950.00</field> </record> @@ -35,7 +34,6 @@ <field name="name">Pen drive, 16GB</field> <field name="product_id" ref="product.product_product_30"/> <field name="product_uom_qty">5</field> - <field name="product_uos_qty">5</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">145.00</field> </record> @@ -45,7 +43,6 @@ <field name="name">Headset USB</field> <field name="product_id" ref="product.product_product_33"/> <field name="product_uom_qty">2</field> - <field name="product_uos_qty">2</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">65.00</field> </record> @@ -56,7 +53,6 @@ <field name="partner_shipping_id" ref="base.res_partner_address_13"/> <field name="user_id" ref="base.user_root"/> <field name="pricelist_id" ref="product.list0"/> - <field name="order_policy">manual</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')"/> </record> @@ -66,7 +62,6 @@ <field name="name">Service on demand</field> <field name="product_id" ref="product.product_product_1"/> <field name="product_uom_qty">24</field> - <field name="product_uos_qty">24</field> <field name="product_uom" ref="product.product_uom_hour"/> <field name="price_unit">75.00</field> </record> @@ -76,7 +71,6 @@ <field name="name">On Site Assistance</field> <field name="product_id" ref="product.product_product_2"/> <field name="product_uom_qty">30</field> - <field name="product_uos_qty">30</field> <field name="product_uom" ref="product.product_uom_hour"/> <field name="price_unit">38.25</field> </record> @@ -87,7 +81,6 @@ <field name="partner_shipping_id" ref="base.res_partner_4"/> <field name="user_id" ref="base.user_root"/> <field name="pricelist_id" ref="product.list0"/> - <field name="order_policy">manual</field> <field name="team_id" ref="sales_team.team_sales_department"/> </record> @@ -96,7 +89,6 @@ <field name="name">On Site Monitoring</field> <field name="product_id" ref="product.product_product_1"/> <field name="product_uom_qty">10</field> - <field name="product_uos_qty">10</field> <field name="product_uom" ref="product.product_uom_hour"/> <field name="price_unit">30.75</field> </record> @@ -106,7 +98,6 @@ <field name="name">Toner Cartridge</field> <field name="product_id" ref="product.product_product_39"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">70.00</field> </record> @@ -125,7 +116,6 @@ <field name="name">Service on demand</field> <field name="product_id" ref="product.product_product_1"/> <field name="product_uom_qty">16</field> - <field name="product_uos_qty">16</field> <field name="product_uom" ref="product.product_uom_hour"/> <field name="price_unit">75.00</field> </record> @@ -135,7 +125,6 @@ <field name="name">Webcam</field> <field name="product_id" ref="product.product_product_34"/> <field name="product_uom_qty">10</field> - <field name="product_uos_qty">10</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">45.00</field> </record> @@ -145,7 +134,6 @@ <field name="name">Multimedia Speakers</field> <field name="product_id" ref="product.product_product_31"/> <field name="product_uom_qty">3</field> - <field name="product_uos_qty">3</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">150.00</field> </record> @@ -155,7 +143,6 @@ <field name="name">Switch, 24 ports</field> <field name="product_id" ref="product.product_product_47"/> <field name="product_uom_qty">2</field> - <field name="product_uos_qty">2</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">70.00</field> </record> @@ -175,7 +162,6 @@ <field name="name">External Hard disk</field> <field name="product_id" ref="product.product_product_28"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">405.00</field> </record> @@ -185,7 +171,6 @@ <field name="name">Blank DVD-RW</field> <field name="product_id" ref="product.product_product_36"/> <field name="product_uom_qty">3</field> - <field name="product_uos_qty">3</field> <field name="product_uom" ref="product.product_uom_dozen"/> <field name="price_unit">24.00</field> </record> @@ -195,7 +180,6 @@ <field name="name">Printer, All-in-one</field> <field name="product_id" ref="product.product_product_37"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">4410.00</field> </record> @@ -214,7 +198,6 @@ <field name="name">PC Assamble + 2GB RAM</field> <field name="product_id" ref="product.product_product_4"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">750.00</field> </record> @@ -225,9 +208,7 @@ <field name="partner_shipping_id" ref="base.res_partner_address_11"/> <field name="user_id" ref="base.user_root"/> <field name="pricelist_id" ref="product.list0"/> - <field name="order_policy">manual</field> <field name="team_id" ref="sales_team.team_sales_department"/> - <field name="date_confirm" eval="(DateTime.today() - relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')"/> </record> <record id="sale_order_line_16" model="sale.order.line"> @@ -235,7 +216,6 @@ <field name="name">Laptop E5023</field> <field name="product_id" ref="product.product_product_25"/> <field name="product_uom_qty">5</field> - <field name="product_uos_qty">5</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">2950.00</field> </record> @@ -245,7 +225,6 @@ <field name="name">GrapWorks Software</field> <field name="product_id" ref="product.product_product_44"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">173.00</field> </record> @@ -255,7 +234,6 @@ <field name="name">Datacard</field> <field name="product_id" ref="product.product_product_46"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">40.00</field> </record> @@ -265,7 +243,6 @@ <field name="name">USB Adapter</field> <field name="product_id" ref="product.product_product_48"/> <field name="product_uom_qty">1</field> - <field name="product_uos_qty">1</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">18.00</field> </record> @@ -277,7 +254,6 @@ <field name="partner_shipping_id" ref="base.res_partner_address_25"/> <field name="user_id" ref="base.user_demo"/> <field name="pricelist_id" ref="product.list0"/> - <field name="order_policy">manual</field> <field name="team_id" ref="sales_team.crm_team_1"/> </record> @@ -286,7 +262,6 @@ <field name="name">Laptop Customized</field> <field name="product_id" ref="product.product_product_27"/> <field name="product_uom_qty">2</field> - <field name="product_uos_qty">2</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">3645.00</field> </record> @@ -296,7 +271,6 @@ <field name="name">Mouse, Wireless</field> <field name="product_id" ref="product.product_product_12"/> <field name="product_uom_qty">2</field> - <field name="product_uos_qty">2</field> <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">12.50</field> </record> @@ -340,13 +314,14 @@ Thanks!</field> <!-- Demo Data for Product --> <record id="advance_product_0" model="product.product"> - <field name="name">Advance</field> + <field name="name">Deposit</field> <field name="categ_id" ref="product.product_category_1"/> <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="product.product_uom_day"/> - <field name="uom_po_id" ref="product.product_uom_day"/> + <field name="uom_id" ref="product.product_uom_unit"/> + <field name="uom_po_id" ref="product.product_uom_unit"/> <field name="company_id" eval="[]"/> <field name="image" type="base64" file="sale/static/img/advance_product_0-image.jpg"/> </record> diff --git a/addons/sale/sale_product_demo.xml b/addons/sale/sale_product_demo.xml new file mode 100644 index 000000000000..43ffa55a7d62 --- /dev/null +++ b/addons/sale/sale_product_demo.xml @@ -0,0 +1,273 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + <data noupdate="1"> + <record id="product.product_product_59" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_58" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_57" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_56" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_55" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_54" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_53" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_52" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_51" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_50" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_49" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_48" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_47" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_46" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_45" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_44" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_43" model="product.product"> + <field name="invoice_policy">order</field> + </record> + + <record id="product.product_product_42" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_41" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_40" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_39" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_38" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_37" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_36" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_35" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_34" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_33" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_32" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_31" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_30" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_29" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_28" 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_26" 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">delivery</field> + </record> + + <record id="product.product_product_23" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_22" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_21" 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_19" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_18" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_17" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_16" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_15" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_14" model="product.product"> + <field name="invoice_policy">delivery</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_5b" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_4d" model="product.product"> + <field name="invoice_policy">delivery</field> + </record> + + <record id="product.product_product_4d" 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> + </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> + + <record id="product.product_product_1b" model="product.product"> + <field name="invoice_policy">cost</field> + </record> + + <record id="product.product_product_0" model="product.product"> + <field name="invoice_policy">order</field> + </record> + + </data> +</openerp> diff --git a/addons/sale/sale_unit_test.xml b/addons/sale/sale_unit_test.xml index 25d7e34c5de8..2bc80d8853fa 100644 --- a/addons/sale/sale_unit_test.xml +++ b/addons/sale/sale_unit_test.xml @@ -17,7 +17,6 @@ <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">450</field> <field name="product_uom_qty">2</field> - <field name="product_uos_qty">2</field> </record> <record id="test_order_1_line_3" model="sale.order.line"> <field name="order_id" ref="test_order_1"/> @@ -26,7 +25,6 @@ <field name="product_uom" ref="product.product_uom_unit"/> <field name="price_unit">90</field> <field name="product_uom_qty">3</field> - <field name="product_uos_qty">3</field> </record> <assert id="test_order_1" model="sale.order" severity="error" string="The amount of the sales order is correctly computed"> diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index 78f01b19b4c8..b60853aa9185 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -44,7 +44,7 @@ <field name="model">sale.order</field> <field name="arch" type="xml"> <pivot string="Sales Orders"> - <field name="partner_id" type="row"/> + <field name="date_order" type="row"/> <field name="amount_total" type="measure"/> </pivot> </field> @@ -59,7 +59,7 @@ <field name="name"/> <field name="partner_id"/> <field name="amount_total"/> - <field name="date_confirm"/> + <field name="date_order"/> <field name="state"/> <templates> <t t-name="kanban-box"> @@ -74,10 +74,10 @@ </div> <div class="row"> <div class="col-xs-6 text-muted"> - <span><t t-esc="record.name.value"/> <t t-esc="record.date_confirm.value"/></span> + <span><t t-esc="record.name.value"/> <t t-esc="record.date_order.value"/></span> </div> <div class="col-xs-6"> - <span t-attf-class="pull-right text-right label #{['draft', 'cancel'].indexOf(record.state.raw_value) > -1 ? 'label-default' : ['except_picking', 'invoice_except'].indexOf(record.state.raw_value) > -1 ? 'label-danger' : ['waiting_date', 'manual'].indexOf(record.state.raw_value) > -1 ? 'label-warning' : ['done'].indexOf(record.state.raw_value) > -1 ? 'label-success' : 'label-primary'}"><t t-esc="record.state.value"/></span> + <span t-attf-class="pull-right text-right label #{['draft', 'cancel'].indexOf(record.state.raw_value) > -1 ? 'label-default' : ['done'].indexOf(record.state.raw_value) > -1 ? 'label-success' : 'label-primary'}"><t t-esc="record.state.value"/></span> </div> </div> </div> @@ -92,14 +92,15 @@ <field name="model">sale.order</field> <field name="priority">2</field> <field name="arch" type="xml"> - <tree string="Sales Orders" decoration-bf="message_needaction==True" decoration-muted="state=='cancel'" decoration-info="state in ('waiting_date','manual')" decoration-danger="state in ('invoice_except','shipping_except')"> + <tree string="Sales Orders" decoration-bf="message_needaction==True" decoration-muted="state=='cancel'"> <field name="message_needaction" invisible="1"/> <field name="name" string="Order Number"/> <field name="date_order"/> <field name="partner_id"/> <field name="user_id"/> <field name="amount_total" sum="Total Tax Included" widget="monetary"/> - <field name="state"/> + <field name="invoice_status"/> + <field name="state" invisible="1"/> </tree> </field> </record> @@ -109,7 +110,7 @@ <field name="model">sale.order</field> <field name="priority">4</field> <field name="arch" type="xml"> - <tree string="Quotation" decoration-bf="message_needaction==True" decoration-muted="state=='cancel'" decoration-info="state in ('waiting_date','manual')" decoration-danger="state in ('invoice_except','shipping_except')"> + <tree string="Quotation" decoration-bf="message_needaction==True" decoration-muted="state=='cancel'"> <field name="message_needaction" invisible="1"/> <field name="name" string="Quotation Number"/> <field name="date_order"/> @@ -126,22 +127,24 @@ <field name="model">sale.order</field> <field name="arch" type="xml"> <form string="Sales Order"> - <header> - <button name="invoice_recreate" states="invoice_except" string="Recreate Invoice" groups="base.group_user"/> - <button name="invoice_corrected" states="invoice_except" string="Ignore Exception" groups="base.group_user"/> - <button name="action_button_confirm" states="sent" string="Confirm Sale" class="oe_highlight" type="object" groups="base.group_user"/> - <button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="oe_highlight" groups="base.group_user"/> - <button name="action_quotation_send" string="Send by Email" type="object" states="sent,progress,manual" groups="base.group_user"/> - <button name="print_quotation" string="Print" type="object" states="draft" class="oe_highlight" groups="base.group_user"/> - <button name="print_quotation" string="Print" type="object" states="sent,progress,manual" groups="base.group_user"/> - <button name="action_button_confirm" states="draft" string="Confirm Sale" type="object" groups="base.group_user"/> - <button name="%(action_view_sale_advance_payment_inv)d" string="Create Invoice" - type="action" states="manual" class="oe_highlight" groups="base.group_user"/> - <button name="copy_quotation" states="cancel" string="New Copy of Quotation" type="object"/> - <button name="cancel" states="draft,sent" string="Cancel" groups="base.group_user"/> - <button name="action_cancel" states="manual,progress" string="Cancel Order" type="object" groups="base.group_user"/> - <button name="invoice_cancel" states="invoice_except" string="Cancel Order" groups="base.group_user"/> - <field name="state" widget="statusbar" statusbar_visible="draft,sent,progress,done" statusbar_colors='{"invoice_except":"red","waiting_date":"blue"}'/> + <header> + <button name="%(action_view_sale_advance_payment_inv)d" string="Create Invoice" + type="action" class="btn-primary" + attrs="{'invisible': [('invoice_status', '!=', 'to invoice')]}"/> + <button name="%(action_view_sale_advance_payment_inv)d" string="Create Invoice" + type="action" context="{'default_advance_payment_method': 'percentage'}" + attrs="{'invisible': ['|',('invoice_status', '!=', 'no'), ('state', '!=', 'sale')]}"/> + <button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="btn-primary"/> + <button name="action_quotation_send" string="Send by Email" type="object" states="sent,sale"/> + <button name="print_quotation" string="Print" type="object" states="draft" class="btn-primary"/> + <button name="print_quotation" string="Print" type="object" states="sent,sale"/> + <button name="action_confirm" states="sent" string="Confirm Sale" class="btn-primary" type="object" context="{'show_sale': True}"/> + <button name="action_confirm" states="draft" string="Confirm Sale" type="object" context="{'show_sale': True}"/> + <button name="action_cancel" states="draft,sent,sale" type="object" string="Cancel"/> + <button name="action_draft" states="cancel" type="object" string="Set to Quotation"/> + <button name="action_done" type="object" string="Set to Done" states="sale" + help="If a sale order is done, you can not modify it manually anymore. But you will still be able to invoice or deliver."/> + <field name="state" widget="statusbar" statusbar_visible="draft,sent,sale,done"/> </header> <sheet> <div class="oe_button_box" name="button_box"> @@ -149,28 +152,25 @@ type="object" class="oe_stat_button" icon="fa-pencil-square-o" - attrs="{'invisible': [('invoice_count', '=', 0)]}" groups="base.group_user"> + attrs="{'invisible': [('invoice_count', '=', 0)]}"> <field name="invoice_count" widget="statinfo" string="Invoices"/> </button> </div> <div class="oe_title"> - <label string="Quotation" attrs="{'invisible': [('state', 'not in', ('draft', 'sent'))]}"/> - <label string="Sales Order" attrs="{'invisible': [('state', 'in', ('draft', 'sent'))]}"/> <h1> <field name="name" class="oe_inline" readonly="1"/> </h1> </div> <group> <group> - <field name="partner_id" on_change="onchange_partner_id(partner_id, context)" domain="[('customer','=',True)]" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/> + <field name="partner_id" domain="[('customer','=',True)]" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/> <field name="partner_invoice_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'invoice'}"/> - <field name="partner_shipping_id" on_change="onchange_delivery_id(company_id, partner_id, partner_shipping_id, fiscal_position_id)" groups="sale.group_delivery_invoice_address" context="{'default_type':'delivery'}"/> - <field name="project_id" context="{'partner_id':partner_invoice_id, 'manager_id': user_id, 'default_pricelist_id':pricelist_id, 'default_name':name, 'default_type': 'contract'}" groups="sale.group_analytic_accounting" domain="[('type','in',['view','normal','contract'])]"/> - <field name="contract_state" groups="sale.group_analytic_accounting" widget="label_selection" attrs="{'invisible': [('project_id', '=', False)]}" options="{'classes': {'template': 'default', 'draft': 'info', 'open': 'success', 'pending': 'warning', 'close': 'danger', 'cancelled': 'warning'}}" readonly="1"/> + <field name="partner_shipping_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'delivery'}"/> + <field name="project_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}"/> </group> <group> - <field name="date_order"/> - <field domain="[('type','=','sale')]" name="pricelist_id" groups="product.group_sale_pricelist" on_change="onchange_pricelist_id(pricelist_id,order_line)"/> + <field name="date_order" invisible="1"/> + <field domain="[('type','=','sale')]" name="pricelist_id" groups="product.group_sale_pricelist"/> <field name="currency_id" invisible="1"/> <field name="validity_date"/> <field name="payment_term_id" options="{'no_create': True}"/> @@ -178,31 +178,35 @@ </group> <notebook> <page string="Order Lines"> - <field name="order_line" mode="tree,kanban"> + <field name="order_line" mode="tree,kanban" + attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"> <form string="Sales Order Lines"> - <header groups="base.group_user"> - <button name="%(action_view_sale_order_line_make_invoice)d" states="confirmed" string="Invoice" type="action" icon="terp-document-new"/> - <field name="state" widget="statusbar" statusbar_visible="draft,confirmed,done" statusbar_colors='{"exception":"red","cancel":"red"}'/> - </header> <group> <group> <field name="product_id" context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" - groups="base.group_user" - on_change="product_id_change(parent.pricelist_id, product_id, product_uom_qty, False, product_uos_qty, False, name, parent.partner_id, False, True, parent.date_order, False, parent.fiscal_position_id, False, context)"/> - <label for="product_uom_qty"/> + attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}" + /> + <field name="invoice_status" invisible="1"/> + <field name="qty_to_invoice" invisible="1"/> + <field name="qty_delivered_updateable" invisible="1"/> + <label for="product_uom_qty" string="Ordered Quantity"/> <div> <field context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'uom_qty_change':True}" - name="product_uom_qty" class="oe_inline" - on_change="product_id_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, False, parent.date_order, False, parent.fiscal_position_id, True, context)"/> + name="product_uom_qty" class="oe_inline"/> <field name="product_uom" groups="product.group_uom" class="oe_inline oe_no_button" - on_change="product_uom_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, False, parent.date_order, context)"/> + attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"/> + </div> + <label for="qty_delivered" string="Delivered Quantity" + invisible="not context.get('show_sale')"/> + <div> + <field name="qty_delivered" invisible="not context.get('show_sale')" + attrs="{'readonly': [('qty_delivered_updateable', '=', False)]}"/> </div> - <label for="product_uos_qty" groups="product.group_uos"/> - <div groups="product.group_uos"> - <field name="product_uos_qty" class="oe_inline"/> - <field name="product_uos" options='{"no_open": True}' class="oe_inline"/> + <label for="qty_invoiced" string="Invoiced Quantity" invisible="not context.get('show_sale')"/> + <div> + <field name="qty_invoiced" invisible="not context.get('show_sale')"/> </div> <field name="price_unit"/> <label for="discount" groups="sale.group_discount_per_so_line"/> @@ -212,7 +216,10 @@ </group> <group> <field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"/> - <field name="th_weight"/> + <label for="customer_lead"/> + <div> + <field name="customer_lead" class="oe_inline"/> days + </div> </group> </group> <label for="name"/> @@ -221,41 +228,43 @@ <label for="invoice_lines"/> <field name="invoice_lines"/> </div> + <field name="state" invisible="1"/> </form> - <tree string="Sales Order Lines" editable="bottom"> + <tree string="Sales Order Lines" editable="bottom" decoration-info="invoice_status=='to invoice'"> <field name="sequence" widget="handle"/> - <field name="state" invisible="1"/> - <field name="th_weight" invisible="1"/> <field name="product_id" + attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}" context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" - groups="base.group_user" - on_change="product_id_change(parent.pricelist_id, product_id, product_uom_qty, False, product_uos_qty, False, name, parent.partner_id, False, True, parent.date_order, False, parent.fiscal_position_id, False, context)"/> + /> <field name="name"/> <field name="product_uom_qty" - context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" - on_change="product_id_change(parent.pricelist_id, product_id, product_uom_qty, product_uom, product_uos_qty, product_uos, name, parent.partner_id, False, False, parent.date_order, False, parent.fiscal_position_id, True, context)"/> + string="Ordered Qty" + context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"/> + <field name="qty_delivered" invisible="not context.get('show_sale')" + attrs="{'readonly': [('qty_delivered_updateable', '=', False)]}"/> + <field name="qty_invoiced" + invisible="not context.get('show_sale')"/> + <field name="qty_to_invoice" invisible="1"/> <field name="product_uom" - on_change="product_uom_change(parent.pricelist_id, product_id, product_uom_qty, product_uom, product_uos_qty, product_uos, name, parent.partner_id, False, False, parent.date_order, context)" + attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}" groups="product.group_uom" options='{"no_open": True}'/> - <field name="product_uos_qty" groups="product.group_uos" invisible="1"/> - <field name="product_uos" string="UoS" groups="product.group_uos" invisible="1"/> - <field name="price_unit"/> + <field name="price_unit" + attrs="{'readonly': [('qty_invoiced', '>', 0)]}"/> <field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"/> <field name="discount" groups="sale.group_discount_per_so_line"/> <field name="price_subtotal" widget="monetary"/> + <field name="qty_delivered_updateable" invisible="1"/> + <field name="state" invisible="1"/> + <field name="invoice_status" invisible="1"/> </tree> <kanban class="o_kanban_mobile"> <field name="product_id" context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" - groups="base.group_user" - on_change="product_id_change(parent.pricelist_id, product_id, product_uom_qty, False, product_uos_qty, False, name, parent.partner_id, False, True, parent.date_order, False, parent.fiscal_position_id, False, context)"/> + /> <field name="product_uom_qty" - context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" - on_change="product_id_change(parent.pricelist_id, product_id, product_uom_qty, product_uom, product_uos_qty, product_uos, name, parent.partner_id, False, False, parent.date_order, False, parent.fiscal_position_id, True, context)"/> + context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"/> <field name="product_uom" - on_change="product_uom_change(parent.pricelist_id, product_id, product_uom_qty, product_uom, product_uos_qty, product_uos, name, parent.partner_id, False, False, parent.date_order, context)" groups="product.group_uom" options='{"no_open": True}'/> - <field name="product_uos_qty" groups="product.group_uos" invisible="1"/> <field name="price_subtotal"/> <templates> <t t-name="kanban-box"> @@ -283,36 +292,36 @@ <field name="amount_tax" widget='monetary' options="{'currency_field': 'currency_id'}"/> <div class="oe_subtotal_footer_separator oe_inline"> <label for="amount_total" /> - <button name="button_dummy" - states="draft,sent" string="(update)" type="object" class="oe_edit_only oe_link"/> + <!-- <button name="button_dummy" + states="draft,sent" string="(update)" type="object" class="oe_edit_only oe_link"/> --> </div> <field name="amount_total" nolabel="1" class="oe_subtotal_footer_separator" widget='monetary' options="{'currency_field': 'currency_id'}"/> </group> - <field name="note" class="oe_inline" placeholder="An administrator can set up default Terms and conditions in your Company settings."/> + <field name="note" class="oe_inline" placeholder="Setup default terms and conditions in your company settings."/> <div class="oe_clear"/> </page> - <page string="Other Information" groups="base.group_user"> + <page string="Other Information"> <group> - <group string="Sales Information" name="sales_person" groups="base.group_user"> + <group string="Sales Information" name="sales_person"> <field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_invoice', 'base.group_sale_salesman_all_leads']}"/> <field name="team_id" options="{'no_create': True}"/> <field name="client_order_ref"/> <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> </group> <group name="sale_pay" string="Invoicing"> - <field name="fiscal_position_id" options="{'no_create': True}" - on_change="onchange_fiscal_position(fiscal_position_id, order_line, context)"/> + <field name="fiscal_position_id" options="{'no_create': True}"/> + <field name="invoice_status" + attrs="{'invisible': [('state', 'not in', ('sale','done'))]}"/> </group> <group string="Reporting" name="technical" groups="base.group_no_one"> <field groups="base.group_no_one" name="origin"/> - <field groups="base.group_no_one" name="invoiced"/> </group> </group> </page> </notebook> </sheet> <div class="oe_chatter"> - <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> + <field name="message_follower_ids" widget="mail_followers"/> <field name="message_ids" widget="mail_thread"/> </div> </form> @@ -344,12 +353,13 @@ <field name="team_id" string="Sales Team"/> <field name="project_id"/> <field name="product_id"/> - <filter string="My" domain="[('user_id','=',uid)]" name="my_sale_orders_filter"/> + <filter string="My Orders" domain="[('user_id','=',uid)]" name="my_sale_orders_filter"/> <separator/> <filter string="Quotations" name="draft" domain="[('state','in',('draft','sent'))]" help="Sales Order that haven't yet been confirmed"/> - <filter string="Sales" name="sales" domain="[('state','in',('manual','progress'))]"/> - <filter string="To Invoice" domain="[('state','=','manual')]" help="Sales Order ready to be invoiced"/> - <filter string="Done" domain="[('state','=','done')]" help="Sales Order done"/> + <filter string="Sales" name="sales" domain="[('state','in',('progress','Done'))]"/> + <separator/> + <filter string="To Invoice" domain="[('invoice_status','=','to invoice')]"/> + <filter string="Upselling" domain="[('invoice_status','=','upselling')]"/> <separator/> <filter string="Important Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/> <group expand="0" string="Group By"> @@ -368,7 +378,7 @@ <field name="view_type">form</field> <field name="view_mode">tree,kanban,form,calendar,pivot,graph</field> <field name="search_view_id" ref="view_sales_order_filter"/> - <field name="context">{}</field> + <field name="context">{'show_sale': True}</field> <field name="domain">[('state', 'not in', ('draft', 'sent', 'cancel'))]</field> <field name="help" type="html"> <p class="oe_view_nocontent_create"> @@ -381,28 +391,37 @@ </field> </record> - <menuitem action="action_orders" id="menu_sale_order" parent="base.menu_sales" sequence="12" groups="base.group_sale_salesman,base.group_sale_manager"/> + <menuitem action="action_orders" + id="menu_sale_order" parent="base.menu_sales" + sequence="12" groups="base.group_sale_salesman,base.group_sale_manager"/> - <record id="action_orders_exception" model="ir.actions.act_window"> - <field name="name">Sales in Exception</field> + <record id="action_orders_to_invoice" model="ir.actions.act_window"> + <field name="name">Sales to Invoice</field> <field name="type">ir.actions.act_window</field> <field name="res_model">sale.order</field> <field name="view_type">form</field> - <field name="view_mode">tree,form,calendar,graph</field> - <field name="domain">[('state','in',('shipping_except','invoice_except'))]</field> - <field name="filter" eval="True"/> - <field name="search_view_id" ref="view_sales_order_filter"/> - </record> - - <record id="action_orders_in_progress" model="ir.actions.act_window"> - <field name="name">Sales Order in Progress</field> + <field name="view_mode">tree,form,calendar,graph,pivot</field> + <field name="domain">[('invoice_status','=','to invoice')]</field> + </record> + <menuitem name="Invoicing" + id="menu_sale_invoicing" parent="base.menu_base_partner" + sequence="5"/> + <menuitem action="action_orders_to_invoice" + id="menu_sale_order_invoice" parent="sale.menu_sale_invoicing" + sequence="2"/> + + <record id="action_orders_upselling" model="ir.actions.act_window"> + <field name="name">Upselling Opportunities</field> <field name="type">ir.actions.act_window</field> <field name="res_model">sale.order</field> <field name="view_type">form</field> - <field name="view_mode">tree,form,calendar,graph</field> - <field name="domain">[('state','in',('progress','waiting_date','manual'))]</field> - <field name="search_view_id" ref="view_sales_order_filter"/> + <field name="view_mode">tree,form,calendar,graph,pivot</field> + <field name="domain">[('invoice_status','=','upselling')]</field> + <field name="context">{'show_sale': True}</field> </record> + <menuitem action="action_orders_upselling" + id="menu_sale_order_upselling" parent="sale.menu_sale_invoicing" + sequence="5"/> <record id="action_quotations" model="ir.actions.act_window"> @@ -433,18 +452,6 @@ action="action_quotations" parent="base.menu_sales" sequence="11"/> - <record id="action_order_tree" model="ir.actions.act_window"> - <field name="name">Old Quotations</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">sale.order</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form,calendar,graph</field> - <field name="domain">[('state','=','draft'),('date_order','<',time.strftime('%Y-%m-%d %H:%M:%S'))]</field> - <field name="filter" eval="True"/> - <field name="search_view_id" ref="view_sales_order_filter"/> - </record> - - <record id="view_order_line_tree" model="ir.ui.view"> <field name="name">sale.order.line.tree</field> <field name="model">sale.order.line</field> @@ -454,63 +461,23 @@ <field name="order_id"/> <field name="order_partner_id"/> <field name="name"/> + <field name="salesman_id"/> <field name="product_uom_qty" string="Qty"/> + <field name="qty_delivered"/> + <field name="qty_invoiced"/> + <field name="qty_to_invoice"/> <field name="product_uom" string="Unit of Measure" groups="product.group_uom"/> - <field name="salesman_id"/> <field name="price_subtotal" sum="Total" widget="monetary"/> - <field name="state"/> - <field name="invoiced"/> </tree> </field> </record> - <record id="view_order_line_form2" model="ir.ui.view"> - <field name="name">sale.order.line.form2</field> - <field name="model">sale.order.line</field> - <field name="arch" type="xml"> - <form string="Sales Order Lines" create="false"> - <header> - <button name="%(action_view_sale_order_line_make_invoice)d" string="Create Invoice" type="action" attrs="{'invisible': ['|',('invoiced', '=', 1), ('state', 'not in', ('confirmed', 'draft'))]}" class="oe_highlight" groups="base.group_user"/> - <button name="button_cancel" string="Cancel Line" type="object" states="confirmed,exception" groups="base.group_user"/> - <button name="button_done" string="Done" type="object" attrs="{'invisible': ['|',('invoiced', '=', 0), ('state', 'not in', ('confirmed', 'exception'))]}" class="oe_highlight" groups="base.group_user"/> - <field name="state" widget="statusbar" statusbar_visible="draft,confirmed,done" statusbar_colors='{"exception":"red","cancel":"red"}'/> - </header> - <sheet> - <label for="order_id" class="oe_edit_only"/> - <h1><field name="order_id" domain="[('state','!=','done')]"/></h1> - <label for="order_partner_id" class="oe_edit_only"/> - <h2><field name="order_partner_id"/></h2> - <group> - <group> - <field name="product_id"/> - <label for="product_uom_qty"/> - <div> - <field name="product_uom_qty" readonly="1" class="oe_inline"/> - <field name="product_uom" groups="product.group_uom" class="oe_inline"/> - </div> - </group> - <group> - <field name="price_unit"/> - <field name="discount" groups="sale.group_discount_per_so_line"/> - <field name="price_subtotal" widget="monetary"/> - <field name="invoiced"/> - <field name="company_id" groups="base.group_multi_company" readonly="1"/> - </group> - </group> - <label for="name"/> - <field name="name"/> - </sheet> - </form> - </field> - </record> <record id="view_sales_order_line_filter" model="ir.ui.view"> <field name="name">sale.order.line.select</field> <field name="model">sale.order.line</field> <field name="arch" type="xml"> <search string="Search Sales Order"> - <filter string="To Invoice" domain="[('invoiced','<>', 1),('state','=','done')]" help="Sales Order Lines ready to be invoiced"/> - <separator/> - <filter string="Confirmed" domain="[('state', 'in', ['confirmed', 'done'])]" name="confirmed"/> + <filter string="To Invoice" domain="[('qty_to_invoice','<>', 0)]" help="Sales Order Lines ready to be invoiced"/> <separator/> <filter string="My Sales Order Lines" domain="[('salesman_id','=',uid)]" help="Sales Order Lines related to a Sales Order of mine"/> <field name="order_id"/> @@ -521,72 +488,18 @@ <filter string="Product" domain="[]" context="{'group_by':'product_id'}"/> <filter string="Order" domain="[]" context="{'group_by':'order_id'}"/> <filter string="Salesperson" domain="[]" context="{'group_by':'salesman_id'}"/> - <filter string="Status" domain="[]" context="{'group_by':'state'}"/> </group> </search> </field> </record> - <record id="view_sales_order_uninvoiced_line_filter" model="ir.ui.view"> - <field name="name">sale.order.uninvoiced.line</field> - <field name="model">sale.order.line</field> - <field name="arch" type="xml"> - <search string="Search Uninvoiced Lines"> - <filter string="To Do" domain="[('state','=','confirmed')]" name="sale order" help="Confirmed sales order lines, not yet delivered"/> - <filter string="Done" domain="[('state','=','done')]" name="sale_order_done" help="Sales order lines done"/> - <filter string="Shipped" domain="[('state','=','done')]" name="unshipped" help="Sales Order Lines that are in 'done' state"/> - <separator/> - <filter string="Uninvoiced" name="uninvoiced" domain="[('invoiced','<>', 1),('state','<>','draft'),('state','<>','cancel')]" help="Sales Order Lines that are confirmed, done or in exception state and haven't yet been invoiced"/> - <separator/> - <filter string="My Sales Order Lines" domain="[('salesman_id','=',uid)]" help="My Sales Order Lines"/> - <field name="order_id"/> - <field name="order_partner_id" operator="child_of"/> - <field name="product_id"/> - <field name="salesman_id"/> - <group expand="0" string="Group By"> - <filter string="Order" domain="[]" context="{'group_by':'order_id'}" help="Order reference"/> - <filter string="Product" domain="[]" context="{'group_by':'product_id'}"/> - <filter string="Status" domain="[]" context="{'group_by':'state'}"/> - </group> - </search> - </field> - </record> - - <record id="action_order_line_tree2" model="ir.actions.act_window"> - <field name="name">Order Lines to Invoice</field> - <field name="type">ir.actions.act_window</field> + <record id="action_product_sale_list" model="ir.actions.act_window"> + <field name="name">Sale Order Lines</field> <field name="res_model">sale.order.line</field> - <field name="view_type">form</field> - <field name="view_mode">tree</field> - <field name="search_view_id" ref="view_sales_order_uninvoiced_line_filter"/> - <field name="context">{"search_default_uninvoiced":1}</field> - <field name="filter" eval="True"/> - <field name="help" type="html"> - <p> - Here is a list of each sales order line to be invoiced. You can - invoice sales orders partially, by lines of sales order. You do - not need this list if you invoice from the delivery orders or - if you invoice sales totally. - </p> - </field> + <field name="context">{'search_default_product_id': [active_id], 'default_product_id': active_id}</field> + <field name="domain">[('state', 'in', ['sale', 'done'])]</field> </record> - <record id="action_order_line_tree3" model="ir.actions.act_window"> - <field name="name">Uninvoiced and Delivered Lines</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">sale.order.line</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="domain">[('invoiced','<>', 1),('state','=','done')]</field> - <field name="filter" eval="True"/> - </record> - <record id="action_order_line_product_tree" model="ir.actions.act_window"> - <field name="context">{}</field><!-- force empty --> - <field name="name">Sales Order Lines</field> - <field name="res_model">sale.order.line</field> - <field name="view_id" ref="view_order_line_tree"/> - <field name="context">{'search_default_confirmed': 1}</field> - </record> <record model="ir.ui.view" id="product_form_view_sale_order_button"> <field name="name">product.product.sale.order</field> <field name="model">product.product</field> @@ -594,12 +507,13 @@ <field name="groups_id" eval="[(4, ref('base.group_sale_salesman'))]"/> <field name="arch" type="xml"> <div name="button_box" position="inside"> - <button class="oe_stat_button" name="action_view_sales" - type="object" icon="fa-usd" + <button class="oe_stat_button" name="%(action_product_sale_list)d" + type="action" icon="fa-usd" groups="base.group_no_one"> <field string="Sales" name="sales_count" widget="statinfo" /> </button> </div> + </field> </record> @@ -610,7 +524,7 @@ <field name="groups_id" eval="[(4, ref('base.group_sale_salesman'))]"/> <field name="arch" type="xml"> <div name="button_box" position="inside"> - <button class="oe_stat_button" name="action_view_sales" + <button class="oe_stat_button" name="action_view_sales" type="object" icon="fa-usd" groups="base.group_no_one"> <field string="Sales" name="sales_count" widget="statinfo" /> @@ -619,6 +533,19 @@ </field> </record> + <record model="ir.ui.view" id="product_template_form_view_invoice_policy"> + <field name="name">product.template.invoice.policy</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_form_view"/> + <field name="arch" type="xml"> + <field name="type" position="after"> + <field name="invoice_policy" widget="radio"/> + <field name="track_service" widget="radio" invisible="True"/> + </field> + </field> + </record> + + <record model="ir.ui.view" id="view_company_inherit_form2"> <field name="name">res.company.form.inherit</field> @@ -672,6 +599,7 @@ <field name="search_view_id" ref="sale.view_sales_order_filter"/> <field name="domain">[('state','not in',('draft','sent','cancel'))]</field> <field name="context">{ + 'show_sale': True, 'search_default_team_id': [active_id], 'default_team_id': active_id, } @@ -693,8 +621,9 @@ <field name="view_type">form</field> <field name="view_mode">tree,form,calendar,graph</field> <field name="search_view_id" ref="sale.view_sales_order_filter"/> - <field name="domain">[('state', '=', 'manual')]</field> + <field name="domain">[('state', '=', 'sale'),('invoice_status','=','to invoice')]</field> <field name="context">{ + 'show_sale': True, 'search_default_team_id': [active_id], 'default_team_id': active_id, } diff --git a/addons/sale/sale_workflow.xml b/addons/sale/sale_workflow.xml deleted file mode 100644 index cae483cf28d7..000000000000 --- a/addons/sale/sale_workflow.xml +++ /dev/null @@ -1,303 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - <record id="wkf_sale" model="workflow"> - <field name="name">sale.order.basic</field> - <field name="osv">sale.order</field> - <field name="on_create">True</field> - </record> - - <!-- Activity --> - <record id="act_draft" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="flow_start">True</field> - <field name="name">draft</field> - </record> - - <record id="act_sent" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">sent</field> - <field name="kind">function</field> - <field name="action">write({'state':'sent'})</field> - </record> - - <record id="act_router" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">router</field> - <field name="kind">function</field> - <field name="action">action_wait()</field> - <field name="split_mode">OR</field> - </record> - - <record id="act_wait_invoice" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">wait_invoice</field> - </record> - - <record id="act_done" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">done</field> - <field name="flow_stop">True</field> - <field name="kind">function</field> - <field name="action">action_done()</field> - <field name="join_mode">AND</field> - </record> - - <record id="act_cancel" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">cancel</field> - <field name="flow_stop">True</field> - <field name="kind">stopall</field> - <field name="action">action_cancel()</field> - </record> - - <record id="act_cancel2" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">cancel2</field> - <field name="flow_stop">True</field> - <field name="kind">stopall</field> - <field name="action">action_cancel()</field> - </record> - - <record id="act_invoice" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">invoice</field> - <field name="kind">subflow</field> - <field name="subflow_id" search="[('name','=','account.invoice.basic')]"/> - <field name="action">action_invoice_create()</field> - </record> - <record id="act_invoice_except" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">invoice_except</field> - <field name="kind">function</field> - <field name="action">action_invoice_cancel()</field> - </record> - <record id="act_invoice_end" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">invoice_end</field> - <field name="kind">function</field> - <field name="action">action_invoice_end()</field> - </record> - <record id="act_invoice_cancel" model="workflow.activity"> - <field name="wkf_id" ref="wkf_sale"/> - <field name="name">invoice_cancel</field> - <field name="flow_stop">True</field> - <field name="kind">stopall</field> - <field name="action">action_cancel()</field> - </record> - - <!-- Transistion --> - - <record id="trans_draft_sent" model="workflow.transition"> - <field name="act_from" ref="act_draft"/> - <field name="act_to" ref="act_sent"/> - <field name="signal">quotation_sent</field> - </record> - <record id="trans_draft_router" model="workflow.transition"> - <field name="act_from" ref="act_draft"/> - <field name="act_to" ref="act_router"/> - <field name="signal">order_confirm</field> - </record> - <record id="trans_draft_cancel" model="workflow.transition"> - <field name="act_from" ref="act_draft"/> - <field name="act_to" ref="act_cancel"/> - <field name="signal">cancel</field> - </record> - <record id="trans_sent_router" model="workflow.transition"> - <field name="act_from" ref="act_sent"/> - <field name="act_to" ref="act_router"/> - <field name="signal">order_confirm</field> - </record> - <record id="trans_sent_cancel" model="workflow.transition"> - <field name="act_from" ref="act_sent"/> - <field name="act_to" ref="act_cancel"/> - <field name="signal">cancel</field> - </record> - <record id="trans_router_wait_invoice" model="workflow.transition"> - <field name="act_from" ref="act_router"/> - <field name="act_to" ref="act_wait_invoice"/> - </record> - <record id="trans_wait_invoice_all_lines_invoiced" model="workflow.transition"> - <field name="act_from" ref="act_wait_invoice"/> - <field name="act_to" ref="act_invoice_end"/> - <field name="signal">all_lines</field> - </record> - <record id="trans_wait_invoice_cancel2" model="workflow.transition"> - <field name="act_from" ref="act_wait_invoice"/> - <field name="act_to" ref="act_cancel2"/> - <field name="signal">cancel</field> - </record> - <record id="trans_wait_invoice_invoice_manual" model="workflow.transition"> - <field name="act_from" ref="act_wait_invoice"/> - <field name="act_to" ref="act_invoice"/> - <field name="signal">manual_invoice</field> - </record> - <record id="trans_invoice_invoice_end" model="workflow.transition"> - <field name="act_from" ref="act_invoice"/> - <field name="act_to" ref="act_invoice_end"/> - <field name="signal">subflow.paid</field> - </record> - <record id="trans_invoice_invoice_except" model="workflow.transition"> - <field name="act_from" ref="act_invoice"/> - <field name="act_to" ref="act_invoice_except"/> - <field name="signal">subflow.cancel</field> - </record> - <record id="trans_invoice_except_invoice" model="workflow.transition"> - <field name="act_from" ref="act_invoice_except"/> - <field name="act_to" ref="act_invoice"/> - <field name="signal">invoice_recreate</field> - </record> - <record id="trans_invoice_except_invoice_end" model="workflow.transition"> - <field name="act_from" ref="act_invoice_except"/> - <field name="act_to" ref="act_invoice_end"/> - <field name="signal">invoice_corrected</field> - </record> - <record id="trans_invoice_except_invoice_cancel" model="workflow.transition"> - <field name="act_from" ref="act_invoice_except"/> - <field name="act_to" ref="act_invoice_cancel"/> - <field name="signal">invoice_cancel</field> - </record> - <record id="trans_invoice_end_done" model="workflow.transition"> - <field name="act_from" ref="act_invoice_end"/> - <field name="act_to" ref="act_done"/> - </record> - - <!-- - Procurements creation and checking branch - --> - - <!-- Activity --> - - <record id="act_wait_ship" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">wait_ship</field> - </record> - - <record id="act_cancel3" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">cancel3</field> - <field name="flow_stop">True</field> - <field name="kind">stopall</field> - <field name="action">action_cancel()</field> - </record> - - <record id="act_ship" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">ship</field> - <field name="kind">function</field> - <field name="action">action_ship_create()</field> - </record> - - <record id="act_ship_ignore" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">ship_ignore</field> - <field name="kind">function</field> - <field name="action">action_ignore_delivery_exception()</field> - </record> - - <record id="act_ship_end" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">ship_end</field> - <field name="kind">dummy</field> - </record> - - <record id="act_ship_cancel" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">ship_cancel</field> - <field name="flow_stop">True</field> - <field name="kind">stopall</field> - <field name="action">action_cancel()</field> - </record> - - <record id="act_ship_except" model="workflow.activity"> - <field name="wkf_id" ref="sale.wkf_sale"/> - <field name="name">ship_except</field> - <field name="kind">function</field> - <field name="action">write({'state':'shipping_except'})</field> - </record> - - - - <!-- Transition --> - - <record id="trans_router_wait_ship" model="workflow.transition"> - <field name="act_from" ref="sale.act_router"/> - <field name="act_to" ref="act_wait_ship"/> - </record> - - <record id="trans_router_wait_invoice_shipping" model="workflow.transition"> - <field name="act_from" ref="sale.act_wait_invoice"/> - <field name="act_to" ref="sale.act_invoice_end"/> - <field name="condition">(order_policy=='picking')</field> - </record> - - <record id="trans_wait_invoice_invoice" model="workflow.transition"> - <field name="act_from" ref="sale.act_wait_invoice"/> - <field name="act_to" ref="sale.act_invoice"/> - <field name="condition">order_policy=='prepaid'</field> - </record> - - <record id="trans_wait_ship_cancel3" model="workflow.transition"> - <field name="act_from" ref="act_wait_ship"/> - <field name="act_to" ref="act_cancel3"/> - <field name="signal">cancel</field> - </record> - - <record id="trans_wait_ship_ship" model="workflow.transition"> - <field name="act_from" ref="act_wait_ship"/> - <field name="act_to" ref="act_ship"/> - <field name="condition">procurement_needed() and ((order_policy!='prepaid') or invoiced)</field> - </record> - <record id="trans_wait_ship_done" model="workflow.transition"> - <field name="act_from" ref="act_wait_ship"/> - <field name="act_to" ref="act_ship_end"/> - <field name="condition">not procurement_needed()</field> - </record> - - <record id="trans_ship_end_done" model="workflow.transition"> - <field name="act_from" ref="act_ship_end"/> - <field name="act_to" ref="sale.act_done"/> - </record> - - <record id="trans_ship_ship_end" model="workflow.transition"> - <field name="act_from" ref="act_ship"/> - <field name="act_to" ref="act_ship_end"/> - <field name="signal">ship_end</field> - <field name="trigger_model" eval="False"/> <!-- Force empty --> - <field name="trigger_expr_id" eval="False"/> <!-- Force empty --> - <field name="condition" eval="True"/> <!-- Force empty --> - </record> - - <record id="trans_ship_ship_except" model="workflow.transition"> - <field name="act_from" ref="act_ship"/> - <field name="act_to" ref="act_ship_except"/> - <field name="signal">ship_except</field> - <field name="condition" eval="True"/> <!-- Force empty --> - </record> - - <record id="trans_ship_except_ship" model="workflow.transition"> - <field name="act_from" ref="act_ship_except"/> - <field name="act_to" ref="act_ship"/> - <field name="signal">ship_recreate</field> - </record> - - <record id="trans_ship_except_ship_ignore" model="workflow.transition"> - <field name="act_from" ref="act_ship_except"/> - <field name="act_to" ref="act_ship_ignore"/> - <field name="signal">ship_corrected</field> - </record> - - <record id="trans_ship_ignore_ship_end" model="workflow.transition"> - <field name="act_from" ref="act_ship_ignore"/> - <field name="act_to" ref="act_ship_end"/> - </record> - - <record id="trans_ship_except_ship_cancel" model="workflow.transition"> - <field name="act_from" ref="act_ship_except"/> - <field name="act_to" ref="act_ship_cancel"/> - <field name="signal">ship_cancel</field> - </record> - - - </data> -</openerp> diff --git a/addons/sale/test/cancel_order.yml b/addons/sale/test/cancel_order.yml deleted file mode 100644 index f893706c47d0..000000000000 --- a/addons/sale/test/cancel_order.yml +++ /dev/null @@ -1,82 +0,0 @@ -- - Salesman can also cancel order therefore test with that user which have salesman rights, -- - !context - uid: 'res_users_salesman' -- - In order to test the cancel sale order. - I confirm order (with at least 2 lines) -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_8} - -- - I check state of order in 'Sale Order'. -- - !assert {model: sale.order, id: sale_order_8, string: Sale order should be In Progress state}: - - state == 'manual' -- - I check that Invoice should not be created. -- - !python {model: sale.order}: | - sale_order = self.browse(cr, uid, ref("sale_order_8")) - assert len(sale_order.invoice_ids) == False, "Invoice should not be created." -- - I create an invoice for the first line -- - !python {model: sale.order.line.make.invoice}: | - - ctx = context.copy() - ctx.update({"active_model": 'sale.order.line', "active_ids": [ref("sale_order_line_20")], "active_id":ref("sale_order_line_20")}) - pay_id = self.create(cr, uid, {}) - self.make_invoices(cr, uid, [pay_id], context=ctx) - invoice_ids = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_8")).invoice_ids -- - I create an invoice for a fixed price (25% of the second line, thus 5) -- - !python {model: sale.advance.payment.inv}: | - ctx = context.copy() - ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order_8")], "active_id":ref("sale_order_8")}) - pay_id = self.create(cr, uid, {'advance_payment_method': 'fixed', 'amount': 5, 'product_id': ref("sale_order_line_21")}) - self.create_invoices(cr, uid, [pay_id], context=ctx) - invoice_ids = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_8")).invoice_ids - -- - I create an invoice for the remaining and check the amount (should be the remaining amount of second line) -- - !python {model: sale.advance.payment.inv}: | - ctx = context.copy() - ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order_8")], "active_id":ref("sale_order_8")}) - pay_id = self.create(cr, uid, {'advance_payment_method': 'all'}) - self.create_invoices(cr, uid, [pay_id], context=ctx) - invoice_ids = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_8")).invoice_ids - assert len(invoice_ids) == 3, "All invoices are not created" - for invoice in invoice_ids: - assert invoice.amount_total in (7290,5,20), "Invoice total is not correct" -- - I cancel all the invoices. -- - !python {model: sale.order}: | - invoice_ids = self.browse(cr, uid, ref("sale_order_8")).invoice_ids - for invoice in invoice_ids: - invoice.signal_workflow('invoice_cancel') -- - I check order status in "Invoice Exception" and related invoice is in cancel state. -- - !assert {model: sale.order, id: sale_order_8, string: Sale order should be in Invoice Exception state}: - - state == "invoice_except", "Order should be in Invoice Exception state after cancel Invoice" -- - I click recreate invoice. -- - !workflow {model: sale.order, action: invoice_recreate, ref: sale_order_8} -- - I check that the invoice is correctly created with all lines. -- - !python {model: sale.order}: | - sale_order = self.browse(cr, uid, ref("sale_order_8")) - total_order_line = 0 - assert len(sale_order.invoice_ids), "Invoice should be created." - for invoice in sale_order.invoice_ids: - if invoice.state != 'cancel': - total_order_line += len(invoice.invoice_line_ids) - assert total_order_line == 2, "wrong number of invoice lines, got %s" % total_order_line - diff --git a/addons/sale/test/canceled_lines_order.yml b/addons/sale/test/canceled_lines_order.yml deleted file mode 100644 index dd260e021a1a..000000000000 --- a/addons/sale/test/canceled_lines_order.yml +++ /dev/null @@ -1,60 +0,0 @@ -- - I create a draft Sale Order with 2 lines but 1 canceled in order to check if the canceled lines are not considered in the logic -- - !record {model: sale.order, id: sale_order_cl_2}: - partner_id: base.res_partner_3 - partner_invoice_id: base.res_partner_address_25 - partner_shipping_id: base.res_partner_address_25 - pricelist_id: product.list0 - order_policy: manual -- - !record {model: sale.order.line, id: sale_order_cl_2_line_1}: - order_id: sale_order_cl_2 - product_id: product.product_product_27 - product_uom_qty: 1 - product_uom: 1 - price_unit: 3645 - name: 'Laptop Customized' -- - !record {model: sale.order.line, id: sale_order_cl_2_line_2}: - order_id: sale_order_cl_2 - product_id: product.product_product_12 - product_uom_qty: 1 - product_uom: 1 - price_unit: 12.50 - name: 'Mouse, Wireless' -- - I cancel the first line -- - !python {model: sale.order.line, id: sale_order_cl_2_line_1}: | - self.button_cancel() -- - I confirm the sale order -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_cl_2} -- - Invoice the whole sale order -- - !python {model: sale.advance.payment.inv}: | - ctx = context.copy() - ctx.update({"active_model": 'sale.order', - "active_ids": [ref("sale_order_cl_2")], - "active_id":ref("sale_order_cl_2")}) - pay_id = self.create(cr, uid, {'advance_payment_method': 'all'}) - self.create_invoices(cr, uid, [pay_id], context=ctx) -- - I check the invoice -- - !python {model: sale.order, id: sale_order_cl_2}: | - invoice = self.invoice_ids - assert len(invoice.invoice_line_ids) == 1, "Only 1 line should be invoiced because the other one is canceled, got %d" % len(invoice.invoice_line_ids) -- - I set the sale to done -- - !python {model: sale.order, id: sale_order_cl_2}: | - self.action_done() -- - And check if the canceled line is still canceled -- - !assert {model: sale.order.line, id: sale_order_cl_2_line_1, string: The canceled line should still be canceled}: - - state == 'cancel' diff --git a/addons/sale/test/create_sale_users.yml b/addons/sale/test/create_sale_users.yml deleted file mode 100644 index 0035b7675dfd..000000000000 --- a/addons/sale/test/create_sale_users.yml +++ /dev/null @@ -1,28 +0,0 @@ -- - Create a user as 'Salesmanager' -- - !record {model: res.users, id: res_users_salesmanager, view: False}: - company_id: base.main_company - name: Sales manager - login: sm - email: salesmanager@yourcompany.com -- - I added groups for Salesmanager. -- - !record {model: res.users, id: res_users_salesmanager}: - groups_id: - - base.group_sale_manager -- - Create a user as 'Salesman' -- - !record {model: res.users, id: res_users_salesman, view: False}: - company_id: base.main_company - name: Salesman - login: su - email: salesman@yourcompany.com -- - I added groups for Salesman. -- - !record {model: res.users, id: res_users_salesman}: - groups_id: - - base.group_sale_salesman_all_leads diff --git a/addons/sale/test/delete_order.yml b/addons/sale/test/delete_order.yml deleted file mode 100644 index 9d8db337d5bf..000000000000 --- a/addons/sale/test/delete_order.yml +++ /dev/null @@ -1,19 +0,0 @@ -- - Sales manager can only delete order therefore test with that user which have sales manager rights, -- - !context - uid: 'res_users_salesmanager' -- - I try to delete In progress order and check Error Message. -- - !python {model: sale.order}: | - try: - self.unlink(cr, uid, [ref("sale_order_7")]) - except Exception,e: - pass -- - I make duplicate order and delete. -- - !python {model: sale.order}: | - id = self.copy(cr, uid, ref('sale_order_7')) - self.unlink(cr, uid, [id]) diff --git a/addons/sale/test/manual_order_policy.yml b/addons/sale/test/manual_order_policy.yml deleted file mode 100644 index a00b0b4fada6..000000000000 --- a/addons/sale/test/manual_order_policy.yml +++ /dev/null @@ -1,66 +0,0 @@ -- - I confirm the Quotation with "On Demand" order policy. -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_2} -- - I check that Invoice should not created. -- - !python {model: sale.order}: | - sale_order = self.browse(cr, uid, ref("sale_order_2")) - assert len(sale_order.invoice_ids) == 0, "Invoice should not created." -- - I create advance invoice where type is 'Fixed Price'. -- - !python {model: sale.advance.payment.inv}: | - ctx = context.copy() - ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order_2")], "active_id":ref("sale_order_2")}) - order_line = self.pool.get('sale.order.line').browse(cr, uid, ref("sale_order_line_4"), context=context) - pay_id = self.create(cr, uid, {'advance_payment_method': 'fixed', 'product_id': order_line.product_id.id, 'amount': order_line.price_unit}) - self.create_invoices(cr, uid, [pay_id], context=ctx) -- - I check Invoice which made advance. -- - !python {model: sale.order}: | - order = self.browse(cr, uid, ref('sale_order_2')) - assert order.invoice_ids, "Invoice should be created after make advance invoice." -- - I create advance invoice where type is 'Invoice all the Sale Order'. -- - !python {model: sale.advance.payment.inv}: | - ctx = context.copy() - ctx.update({"active_model": 'sale.order', "active_ids": [ref("sale_order_2")], "active_id":ref("sale_order_2")}) - pay_id = self.create(cr, uid, {'advance_payment_method': 'all'}) - self.create_invoices(cr, uid, [pay_id], context=ctx) -- - I check Invoice which made advance where type is 'Invoice all the Sale Order'. -- - !python {model: sale.order}: | - order = self.browse(cr, uid, ref('sale_order_2')) - assert order.invoice_ids, "Invoice should be created after make advance invoice where type is 'Invoice all the Sale Order'." -- - I open the Invoice. -- - !python {model: sale.order}: | - so = self.browse(cr, uid, ref("sale_order_2")) - account_invoice_obj = self.pool.get('account.invoice') - for invoice in so.invoice_ids: - invoice.signal_workflow('invoice_open') -- - I pay the invoice. -- - !python {model: account.invoice}: | - sale_order = self.pool.get('sale.order') - order = sale_order.browse(cr, uid, ref("sale_order_2")) - journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1) - import time - for invoice in order.invoice_ids: - self.pay_and_reconcile(cr, uid, [invoice.id], journal_ids[0]) -- - I check Invoice after do manual. -- - !python {model: sale.order}: | - sale_order = self.browse(cr, uid, ref("sale_order_2")) - assert sale_order.invoice_ids, "Invoice should be created." - assert sale_order.invoice_count, "Order is not invoiced." - assert sale_order.invoiced, "Order is not paid." - assert sale_order.state == 'done', 'Order should be Done.' diff --git a/addons/sale/test/sale_order_demo.yml b/addons/sale/test/sale_order_demo.yml deleted file mode 100644 index 0c849a391616..000000000000 --- a/addons/sale/test/sale_order_demo.yml +++ /dev/null @@ -1,46 +0,0 @@ -- - Test the data with salesman, -- - !context - uid: 'res_users_salesman' -- - In order to test process of the Sale Order, I create sale order -- - !record {model: sale.order, id: sale_order_test1}: - partner_id: base.res_partner_2 - note: Invoice after delivery - payment_term_id: account.account_payment_term - order_line: - - product_id: product.product_product_7 - product_uom_qty: 8 -- - I verify that the onchange was correctly triggered -- - !python {model: sale.order}: | - from openerp.tools import float_compare - order_line = self.browse(cr, uid, ref('sale.sale_order_test1')).order_line - assert order_line[0].name == u'[A8767] Apple In-Ear Headphones', "The onchange function of product was not correctly triggered" - assert float_compare(order_line[0].price_unit, 79.0, precision_digits=2) == 0, "The onchange function of product was not correctly triggered" - assert order_line[0].product_uom_qty == 8, "The onchange function of product was not correctly triggered" - assert order_line[0].product_uom.id == ref('product.product_uom_unit'), "The onchange function of product was not correctly triggered" - -- - I create another sale order -- - !record {model: sale.order, id: sale_order_test2}: - partner_id: base.res_partner_2 - order_line: - - product_id: product.product_product_7 - product_uom_qty: 16 - product_uom: product.product_uom_dozen -- - I verify that the onchange was correctly triggered -- - !python {model: sale.order}: | - from openerp.tools import float_compare - order_line = self.browse(cr, uid, ref('sale.sale_order_test2')).order_line - assert order_line[0].name == u'[A8767] Apple In-Ear Headphones', "The onchange function of product was not correctly triggered" - assert float_compare(order_line[0].price_unit, 79.0 * 12, precision_digits=2) == 0, "The onchange function of product was not correctly triggered" - assert order_line[0].product_uom.id == ref('product.product_uom_dozen'), "The onchange function of product was not correctly triggered" - assert order_line[0].product_uom_qty == 16, "The onchange function of product was not correctly triggered" - diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 5ce968f63de2..29f17446e860 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import test_sale_to_invoice +import test_sale_to_invoice +import test_sale_order diff --git a/addons/sale/tests/test_sale_common.py b/addons/sale/tests/test_sale_common.py new file mode 100644 index 000000000000..7314fb0d78b4 --- /dev/null +++ b/addons/sale/tests/test_sale_common.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from openerp.tests import common + + +class TestSale(common.TransactionCase): + def setUp(self): + super(TestSale, self).setUp() + # some users + group_manager = self.env.ref('base.group_sale_manager') + group_user = self.env.ref('base.group_sale_salesman') + self.manager = self.env['res.users'].create({ + 'name': 'Andrew Manager', + 'login': 'manager', + 'alias_name': 'andrew', + 'email': 'a.m@example.com', + 'signature': '--\nAndreww', + 'notify_email': 'always', + 'groups_id': [(6, 0, [group_manager.id])] + }) + self.user = self.env['res.users'].create({ + 'name': 'Mark User', + 'login': 'user', + 'alias_name': 'mark', + 'email': 'm.u@example.com', + 'signature': '--\nMark', + 'notify_email': 'always', + 'groups_id': [(6, 0, [group_user.id])] + }) + # create quotation with differend kinds of products (all possible combinations) + self.products = { + 'prod_order': self.env.ref('product.product_product_43'), + 'prod_del': self.env.ref('product.product_product_47'), + 'serv_order': self.env.ref('product.product_product_0'), + 'serv_del': self.env.ref('product.product_product_56'), + } + + self.partner = self.env.ref('base.res_partner_1') diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py new file mode 100644 index 000000000000..0b55dacf9b26 --- /dev/null +++ b/addons/sale/tests/test_sale_order.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from openerp.exceptions import UserError, AccessError + +from test_sale_common import TestSale + + +class TestSaleOrder(TestSale): + def test_sale_order(self): + """ Test the sale order flow (invoicing and quantity updates) + - Invoice repeatedly while varrying delivered quantities and check that invoice are always what we expect + """ + # DBO TODO: validate invoice and register payments + inv_obj = self.env['account.invoice'] + so = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in self.products.iteritems()], + 'pricelist_id': self.env.ref('product.list0').id, + }) + self.assertEqual(so.amount_total, sum([2 * p.list_price for (k, p) in self.products.iteritems()]), 'Sale: total amount is wrong') + + # send quotation + so.force_quotation_send() + self.assertTrue(so.state == 'sent', 'Sale: state after sending is wrong') + + # confirm quotation + so.action_confirm() + self.assertTrue(so.state == 'sale') + self.assertTrue(so.invoice_status == 'to invoice') + + # create invoice: only 'invoice on order' products are invoiced + inv_id = so.action_invoice_create() + inv = inv_obj.browse(inv_id) + self.assertEqual(len(inv.invoice_line_ids), 2, 'Sale: invoice is missing lines') + self.assertEqual(inv.amount_total, sum([2 * p.list_price if p.invoice_policy == 'order' else 0 for (k, p) in self.products.iteritems()]), 'Sale: invoice total amount is wrong') + self.assertTrue(so.invoice_status == 'no', 'Sale: SO status after invoicing should be "nothing to invoice"') + self.assertTrue(len(so.invoice_ids) == 1, 'Sale: invoice is missing') + + # deliver lines except 'time and material' then invoice again + for line in so.order_line: + line.qty_delivered = 2 if line.product_id.invoice_policy in ['order', 'delivery'] else 0 + self.assertTrue(so.invoice_status == 'to invoice', 'Sale: SO status after delivery should be "to invoice"') + inv_id = so.action_invoice_create() + inv = inv_obj.browse(inv_id) + self.assertEqual(len(inv.invoice_line_ids), 2, 'Sale: second invoice is missing lines') + self.assertEqual(inv.amount_total, sum([2 * p.list_price if p.invoice_policy == 'delivery' else 0 for (k, p) in self.products.iteritems()]), 'Sale: second invoice total amount is wrong') + self.assertTrue(so.invoice_status == 'invoiced', 'Sale: SO status after invoicing everything should be "invoiced"') + self.assertTrue(len(so.invoice_ids) == 2, 'Sale: invoice is missing') + # go over the sold quantity + for line in so.order_line: + if line.product_id == self.products['serv_order']: + line.qty_delivered = 10 + self.assertTrue(so.invoice_status == 'upselling', 'Sale: SO status after increasing delivered qty higher than ordered qty should be "upselling"') + + # upsell and invoice + for line in so.order_line: + if line.product_id == self.products['serv_order']: + line.product_uom_qty = 10 + inv_id = so.action_invoice_create() + inv = inv_obj.browse(inv_id) + self.assertEqual(len(inv.invoice_line_ids), 1, 'Sale: third invoice is missing lines') + self.assertEqual(inv.amount_total, 8 * self.products['serv_order'].list_price, 'Sale: second invoice total amount is wrong') + self.assertTrue(so.invoice_status == 'invoiced', 'Sale: SO status after invoicing everything (including the upsel) should be "invoiced"') + + def test_unlink_cancel(self): + """ Test deleting and cancelling sale orders depending on their state and on the user's rights """ + so = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in self.products.iteritems()], + 'pricelist_id': self.env.ref('product.list0').id, + }) + # only quotations are deletable + with self.assertRaises(UserError): + so.action_confirm() + so.unlink() + so_copy = so.copy() + with self.assertRaises(AccessError): + so_copy.sudo(self.user).unlink() + self.assertTrue(so_copy.sudo(self.manager).unlink(), 'Sale: deleting a quotation should be possible') + + # cancelling and setting to done, you should not be able to delete any SO ever + so.action_cancel() + self.assertTrue(so.state == 'cancel', 'Sale: cancelling SO should always be possible') + with self.assertRaises(UserError): + so.sudo(self.manager).unlink() + so.action_done() + self.assertTrue(so.state == 'done', 'Sale: SO not done') + + def test_cost_invoicing(self): + """ Test confirming a vendor invoice to reinvoice cost on the so """ + serv_cost = self.env.ref('product.product_product_1b') + prod_gap = self.env.ref('product.product_product_1') + so = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': prod_gap.name, 'product_id': prod_gap.id, 'product_uom_qty': 2, 'product_uom': prod_gap.uom_id.id, 'price_unit': prod_gap.list_price})], + 'pricelist_id': self.env.ref('product.list0').id, + }) + so.action_confirm() + so._create_analytic_account() + inv_partner = self.env.ref('base.res_partner_2') + company = self.env.ref('base.main_company') + journal = self.env['account.journal'].create({'name': 'Purchase Journal - Test', 'code': 'STPJ', 'type': 'purchase', 'company_id': company.id}) + account_payable = self.env['account.account'].create({'code': 'X1111', 'name': 'Sale - Test Payable Account', 'user_type_id': self.env.ref('account.data_account_type_payable').id, 'reconcile': True}) + account_income = self.env['account.account'].create({'code': 'X1112', 'name': 'Sale - Test Account', 'user_type_id': self.env.ref('account.data_account_type_direct_costs').id}) + invoice_vals = { + 'name': '', + 'type': 'in_invoice', + 'partner_id': inv_partner.id, + 'invoice_line_ids': [(0, 0, {'name': serv_cost.name, 'product_id': serv_cost.id, 'quantity': 2, 'uom_id': serv_cost.uom_id.id, 'price_unit': serv_cost.standard_price, 'account_analytic_id': so.project_id.id, 'account_id': account_income.id})], + 'account_id': account_payable.id, + 'journal_id': journal.id, + 'currency_id': company.currency_id.id, + } + inv = self.env['account.invoice'].create(invoice_vals) + inv.signal_workflow('invoice_open') + sol = so.order_line.filtered(lambda l: l.product_id == serv_cost) + self.assertTrue(sol, 'Sale: cost invoicing does not add lines when confirming vendor invoice') + self.assertTrue(sol.price_unit == 160 and sol.qty_delivered == 2 and sol.product_uom_qty == sol.qty_invoiced == 0, 'Sale: line is wrong after confirming vendor invoice') diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index 47b6a7b20bb5..342a490c68cb 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -20,7 +20,6 @@ class TestSale(TestMail): account_obj = self.env['account.account'] # Usefull record id group_id = IrModelData.xmlid_to_res_id('account.group_account_invoice') or False - product_id = IrModelData.xmlid_to_res_id('product.product_category_3') or False company_id = IrModelData.xmlid_to_res_id('base.main_company') or False # Usefull accounts @@ -53,13 +52,13 @@ class TestSale(TestMail): # In order to test I create sale order and confirmed it. order = self.env['sale.order'].create({ 'partner_id': partner.id, - 'date_order': datetime.today()}) - order_line = self.env['sale.order.line'].create({ - 'order_id': order.id, - 'product_id': product_id}) + 'partner_invoice_id': partner.id, + 'partner_shipping_id': partner.id, + 'date_order': datetime.today(), + 'pricelist_id': self.env.ref('product.list0').id}) assert order, "Sale order will not created." context = {"active_model": 'sale.order', "active_ids": [order.id], "active_id": order.id} - order.with_context(context).action_button_confirm() + order.with_context(context).action_confirm() # Now I create invoice. payment = self.env['sale.advance.payment.inv'].create({'advance_payment_method': 'fixed', 'amount': 5}) invoice = payment.with_context(context).create_invoices() diff --git a/addons/sale/views/report_saleorder.xml b/addons/sale/views/report_saleorder.xml index 05cf41262a58..928d83f2a399 100644 --- a/addons/sale/views/report_saleorder.xml +++ b/addons/sale/views/report_saleorder.xml @@ -67,28 +67,30 @@ </tr> </thead> <tbody class="sale_tbody"> - <tr t-foreach="doc.order_line" t-as="l"> - <td> - <span t-field="l.name"/> - </td> - <td class="text-right"> - <span t-field="l.product_uom_qty"/> - <span groups="product.group_uom" t-field="l.product_uom"/> - </td> - <td class="text-right"> - <span t-field="l.price_unit"/> - </td> - <td t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line"> - <span t-field="l.discount"/> - </td> - <td> - <span t-esc="', '.join(map(lambda x: x.name, l.tax_id))"/> - </td> - <td class="text-right"> - <span t-field="l.price_subtotal" - t-field-options='{"widget": "monetary", "display_currency": "doc.pricelist_id.currency_id"}'/> - </td> - </tr> + <t t-foreach="doc.order_line" t-as="l"> + <tr t-if="l.product_uom_qty"> + <td> + <span t-field="l.name"/> + </td> + <td class="text-right"> + <span t-field="l.product_uom_qty"/> + <span groups="product.group_uom" t-field="l.product_uom"/> + </td> + <td class="text-right"> + <span t-field="l.price_unit"/> + </td> + <td t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line"> + <span t-field="l.discount"/> + </td> + <td> + <span t-esc="', '.join(map(lambda x: x.name, l.tax_id))"/> + </td> + <td class="text-right"> + <span t-field="l.price_subtotal" + t-field-options='{"widget": "monetary", "display_currency": "doc.pricelist_id.currency_id"}'/> + </td> + </tr> + </t> </tbody> </table> diff --git a/addons/sale/wizard/__init__.py b/addons/sale/wizard/__init__.py index 529f79adf42c..92be178df5ad 100644 --- a/addons/sale/wizard/__init__.py +++ b/addons/sale/wizard/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import sale_make_invoice -import sale_line_invoice import sale_make_invoice_advance diff --git a/addons/sale/wizard/sale_line_invoice.py b/addons/sale/wizard/sale_line_invoice.py deleted file mode 100644 index f06356c3faf0..000000000000 --- a/addons/sale/wizard/sale_line_invoice.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import osv, fields -from openerp.tools.translate import _ -from openerp import workflow -from openerp.exceptions import UserError - -class sale_order_line_make_invoice(osv.osv_memory): - _name = "sale.order.line.make.invoice" - _description = "Sale OrderLine Make_invoice" - - def _prepare_invoice(self, cr, uid, order, lines, context=None): - a = order.partner_id.property_account_receivable_id.id - if order.partner_id and order.partner_id.property_payment_term_id.id: - pay_term = order.partner_id.property_payment_term_id.id - else: - pay_term = False - return { - 'name': order.client_order_ref or '', - 'origin': order.name, - 'type': 'out_invoice', - 'reference': "P%dSO%d" % (order.partner_id.id, order.id), - 'account_id': a, - 'partner_id': order.partner_invoice_id.id, - 'invoice_line_ids': [(6, 0, lines)], - 'currency_id' : order.pricelist_id.currency_id.id, - 'comment': order.note, - 'payment_term_id': pay_term, - 'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id, - 'user_id': order.user_id and order.user_id.id or False, - 'company_id': order.company_id and order.company_id.id or False, - 'date_invoice': fields.date.today(), - 'team_id': order.team_id.id, - } - - - def make_invoices(self, cr, uid, ids, context=None): - """ - To make invoices. - - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param ids: the ID or list of IDs - @param context: A standard dictionary - - @return: A dictionary which of fields with values. - - """ - if context is None: context = {} - res = False - invoices = {} - - #TODO: merge with sale.py/make_invoice - def make_invoice(order, lines): - """ - To make invoices. - - @param order: - @param lines: - - @return: - - """ - inv = self._prepare_invoice(cr, uid, order, lines) - inv_id = self.pool.get('account.invoice').create(cr, uid, inv) - return inv_id - - sales_order_line_obj = self.pool.get('sale.order.line') - sales_order_obj = self.pool.get('sale.order') - for line in sales_order_line_obj.browse(cr, uid, context.get('active_ids', []), context=context): - if (not line.invoiced) and (line.state not in ('draft', 'cancel')): - if not line.order_id in invoices: - invoices[line.order_id] = [] - line_id = sales_order_line_obj.invoice_line_create(cr, uid, [line.id]) - for lid in line_id: - invoices[line.order_id].append(lid) - for order, il in invoices.items(): - res = make_invoice(order, il) - cr.execute('INSERT INTO sale_order_invoice_rel \ - (order_id,invoice_id) values (%s,%s)', (order.id, res)) - sales_order_obj.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context) - flag = True - sales_order_obj.message_post(cr, uid, [order.id], body=_("Invoice created"), context=context) - data_sale = sales_order_obj.browse(cr, uid, order.id, context=context) - for line in data_sale.order_line: - if not line.invoiced and line.state != 'cancel': - flag = False - break - if flag: - line.order_id.write({'state': 'progress'}) - workflow.trg_validate(uid, 'sale.order', order.id, 'all_lines', cr) - - if not invoices: - raise UserError(_('Invoice cannot be created for this Sales Order Line due to one of the following reasons:\n1.The state of this sales order line is either "draft" or "cancel"!\n2.The Sales Order Line is Invoiced!')) - if context.get('open_invoices', False): - return self.open_invoices(cr, uid, ids, res, context=context) - return {'type': 'ir.actions.act_window_close'} - - def open_invoices(self, cr, uid, ids, invoice_ids, context=None): - """ open a view on one of the given invoice_ids """ - ir_model_data = self.pool.get('ir.model.data') - form_res = ir_model_data.get_object_reference(cr, uid, 'account', 'invoice_form') - form_id = form_res and form_res[1] or False - tree_res = ir_model_data.get_object_reference(cr, uid, 'account', 'invoice_tree') - tree_id = tree_res and tree_res[1] or False - - return { - 'name': _('Invoice'), - 'view_type': 'form', - 'view_mode': 'form,tree', - 'res_model': 'account.invoice', - 'res_id': invoice_ids, - 'view_id': False, - 'views': [(form_id, 'form'), (tree_id, 'tree')], - 'context': {'type': 'out_invoice'}, - 'type': 'ir.actions.act_window', - } diff --git a/addons/sale/wizard/sale_line_invoice.xml b/addons/sale/wizard/sale_line_invoice.xml deleted file mode 100644 index 148810992c2d..000000000000 --- a/addons/sale/wizard/sale_line_invoice.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - <record id="view_sale_order_line_make_invoice" model="ir.ui.view"> - <field name="name">Sales OrderLine Make Invoice</field> - <field name="model">sale.order.line.make.invoice</field> - <field name="arch" type="xml"> - <form string="Create invoices"> - <p class="oe_grey"> - All items in these order lines will be invoiced. You can also invoice a percentage of the sales order - or a fixed price (for advances) directly from the sales order form if you prefer. - </p> - <footer> - <button name="make_invoices" string="Create & View Invoice" type="object" - context="{'open_invoices': True}" class="oe_highlight"/> - <button name="make_invoices" string="Create Invoices" type="object" class="btn-primary"/> - <button string="Cancel" class="btn-default" special="cancel" /> - </footer> - </form> - </field> - </record> - - <record id="action_view_sale_order_line_make_invoice" model="ir.actions.act_window"> - <field name="name">Create Invoice</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">sale.order.line.make.invoice</field> - <field name="view_type">form</field> - <field name="view_mode">form</field> - <field name="view_id" ref="view_sale_order_line_make_invoice"/> - <field name="target">new</field> - </record> - - <record model="ir.values" id="sale_order_line_make_invoice"> - <field name="model_id" ref="sale.model_sale_order_line" /> - <field name="name">Make Invoices</field> - <field name="key2">client_action_multi</field> - <field name="value" eval="'ir.actions.act_window,' + str(ref('action_view_sale_order_line_make_invoice'))" /> - <field name="key">action</field> - <field name="model">sale.order.line</field> - </record> - </data> -</openerp> diff --git a/addons/sale/wizard/sale_make_invoice.py b/addons/sale/wizard/sale_make_invoice.py deleted file mode 100644 index fd1427d45951..000000000000 --- a/addons/sale/wizard/sale_make_invoice.py +++ /dev/null @@ -1,52 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError - -class sale_make_invoice(osv.osv_memory): - _name = "sale.make.invoice" - _description = "Sales Make Invoice" - _columns = { - 'grouped': fields.boolean('Group the invoices', help='Check the box to group the invoices for the same customers'), - 'invoice_date': fields.date('Invoice Date'), - } - _defaults = { - 'grouped': False, - 'invoice_date': fields.date.context_today, - } - - def view_init(self, cr, uid, fields_list, context=None): - if context is None: - context = {} - record_id = context and context.get('active_id', False) - order = self.pool.get('sale.order').browse(cr, uid, record_id, context=context) - if order.state == 'draft': - raise UserError(_('You cannot create invoice when sales order is not confirmed.')) - return False - - def make_invoices(self, cr, uid, ids, context=None): - order_obj = self.pool.get('sale.order') - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - newinv = [] - if context is None: - context = {} - data = self.read(cr, uid, ids)[0] - for sale_order in order_obj.browse(cr, uid, context.get(('active_ids'), []), context=context): - if sale_order.state != 'manual': - raise UserError(_("You shouldn't manually invoice the following sale order %s") % (sale_order.name)) - - order_obj.action_invoice_create(cr, uid, context.get(('active_ids'), []), data['grouped'], date_invoice=data['invoice_date']) - orders = order_obj.browse(cr, uid, context.get(('active_ids'), []), context=context) - for o in orders: - for i in o.invoice_ids: - newinv.append(i.id) - # Dummy call to workflow, will not create another invoice but bind the new invoice to the subflow - order_obj.signal_workflow(cr, uid, [o.id for o in orders if o.order_policy == 'manual'], 'manual_invoice') - result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1') - id = result and result[1] or False - result = act_obj.read(cr, uid, [id], context=context)[0] - result['domain'] = "[('id','in', [" + ','.join(map(str, newinv)) + "])]" - - return result diff --git a/addons/sale/wizard/sale_make_invoice.xml b/addons/sale/wizard/sale_make_invoice.xml deleted file mode 100644 index 4beaadde2092..000000000000 --- a/addons/sale/wizard/sale_make_invoice.xml +++ /dev/null @@ -1,42 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - <record id="view_sale_order_make_invoice" model="ir.ui.view"> - <field name="name">Create invoices</field> - <field name="model">sale.make.invoice</field> - <field name="arch" type="xml"> - <form string="Create invoices"> - <separator colspan="4" string="Do you really want to create the invoice(s)?" /> - <group> - <field name="grouped"/> - <field name="invoice_date"/> - </group> - <footer> - <button name="make_invoices" string="Create Invoices" type="object" class="btn-primary"/> - <button string="Cancel" class="btn-default" special="cancel" /> - </footer> - </form> - </field> - </record> - - <record id="action_sale_order_make_invoice" model="ir.actions.act_window"> - <field name="name">Make Invoices</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">sale.make.invoice</field> - <field name="view_type">form</field> - <field name="view_mode">form</field> - <field name="view_id" ref="view_sale_order_make_invoice"/> - <field name="target">new</field> - <field name="multi">True</field> - </record> - - <record model="ir.values" id="sale_order_make_invoice"> - <field name="model_id" ref="sale.model_sale_order" /> - <field name="name">Make Invoices</field> - <field name="key2">client_action_multi</field> - <field name="value" eval="'ir.actions.act_window,' + str(ref('action_sale_order_make_invoice'))" /> - <field name="key">action</field> - <field name="model">sale.order</field> - </record> - </data> -</openerp> diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index add69e5839a5..d79b0d4f3aab 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -1,197 +1,135 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv -from openerp.tools.translate import _ +import time + +from openerp import api, fields, models, _ import openerp.addons.decimal_precision as dp from openerp.exceptions import UserError -class sale_advance_payment_inv(osv.osv_memory): +class SaleAdvancePaymentInv(models.TransientModel): _name = "sale.advance.payment.inv" _description = "Sales Advance Payment Invoice" - _columns = { - 'advance_payment_method':fields.selection( - [('all', 'Invoice the whole sales order'), ('percentage','Percentage'), ('fixed','Fixed price (deposit)'), - ('lines', 'Some order lines')], - 'What do you want to invoice?', required=True, - help="""Use Invoice the whole sale order to create the final invoice.\nUse Percentage to invoice a percentage of the total amount.\nUse Fixed Price to invoice a specific amount in advance.\nUse Some Order Lines to invoice a selection of the sales order lines."""), - 'qtty': fields.float('Quantity', digits=(16, 2), required=True), - 'product_id': fields.many2one('product.product', 'Advance Product', - domain=[('type', '=', 'service')], - help="Select a product of type service which is called 'Advance Product'.\nYou may have to create it and set it as a default value on this field."), - 'amount': fields.float('Advance Amount', digits=0, - help="The amount to be invoiced in advance. \nTaxes are not taken into account for advance invoices."), - } - - def _get_advance_product(self, cr, uid, context=None): + @api.model + def _count(self): + return len(self._context.get('active_ids', [])) + + @api.model + def _get_advance_payment_method(self): + if self._count() == 1: + sale_obj = self.env['sale.order'] + order = sale_obj.browse(self._context.get('active_ids'))[0] + if all([line.product_id.invoice_policy == 'order' for line in order.order_line]): + return 'all' + return 'delivered' + + @api.model + def _get_advance_product(self): try: - product = self.pool.get('ir.model.data').get_object(cr, uid, 'sale', 'advance_product_0') + return self.env['ir.model.data'].xmlid_to_res_id('sale.advance_product_0', raise_if_not_found=True) except ValueError: - # a ValueError is returned if the xml id given is not found in the table ir_model_data return False - return product.id - - _defaults = { - 'advance_payment_method': 'all', - 'qtty': 1.0, - 'product_id': _get_advance_product, - } - - def _translate_advance(self, cr, uid, percentage=False, context=None): - return _("Advance of %s %%") if percentage else _("Advance of %s %s") - - def onchange_method(self, cr, uid, ids, advance_payment_method, product_id, context=None): - if advance_payment_method == 'percentage': - return {'value': {'amount':0, 'product_id':False }} - if product_id: - product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) - return {'value': {'amount': product.list_price}} - return {'value': {'amount': 0}} - - def _prepare_advance_invoice_vals(self, cr, uid, ids, context=None): - if context is None: - context = {} - sale_obj = self.pool.get('sale.order') - ir_property_obj = self.pool.get('ir.property') - fiscal_obj = self.pool.get('account.fiscal.position') - inv_line_obj = self.pool.get('account.invoice.line') - invoice_obj = self.pool.get('account.invoice') - wizard = self.browse(cr, uid, ids[0], context) - sale_ids = context.get('active_ids', []) - - result = [] - for sale in sale_obj.browse(cr, uid, sale_ids, context=context): - new_invoice = invoice_obj.new(cr, uid, { - 'invoice_line_ids':[(0, 0, {'product_id': wizard.product_id.id})], - 'partner_id': sale.partner_id.id, - 'fiscal_position_id': sale.fiscal_position_id.id, - 'type': 'out_invoice', - }) - inv_line = new_invoice.invoice_line_ids[0] - inv_line.invoice_id = new_invoice #Little hack to in order to old <-> new api - inv_line._onchange_product_id() - - res = inv_line._convert_to_write(inv_line._cache) - - # determine and check income account - if not wizard.product_id.id : - prop = ir_property_obj.get(cr, uid, - 'property_account_income_categ_id', 'product.category', context=context) - prop_id = prop and prop.id or False - account_id = fiscal_obj.map_account(cr, uid, sale.fiscal_position_id or False, prop_id) - if not account_id: - raise UserError( - _('There is no income account defined as global property.')) - res['account_id'] = account_id - if not res.get('account_id'): - raise UserError( - _('There is no income account defined for this product: "%s" (id:%d).') % \ - (wizard.product_id.name, wizard.product_id.id,)) - # determine invoice amount - if wizard.amount <= 0.00: - raise UserError(_('The value of Advance Amount must be positive.')) - if wizard.advance_payment_method == 'percentage': - inv_amount = sale.amount_untaxed * wizard.amount / 100 - if not res.get('name'): - res['name'] = self._translate_advance(cr, uid, percentage=True, context=dict(context, lang=sale.partner_id.lang)) % (wizard.amount) - else: - inv_amount = wizard.amount - if not res.get('name'): - #TODO: should find a way to call formatLang() from rml_parse - symbol = sale.pricelist_id.currency_id.symbol - if sale.pricelist_id.currency_id.position == 'after': - symbol_order = (inv_amount, symbol) - else: - symbol_order = (symbol, inv_amount) - res['name'] = self._translate_advance(cr, uid, context=dict(context, lang=sale.partner_id.lang)) % symbol_order - - # create the invoice - inv_line_values = { - 'name': res.get('name'), - 'origin': sale.name, - 'account_id': res['account_id'], - 'price_unit': inv_amount, - 'quantity': wizard.qtty or 1.0, - 'discount': False, - 'uos_id': res.get('uos_id', False), - 'product_id': wizard.product_id.id, - 'invoice_line_tax_ids': res.get('invoice_line_tax_ids'), - 'account_analytic_id': sale.project_id.id or False, - } - inv_values = { - 'name': sale.client_order_ref or sale.name, - 'origin': sale.name, - 'type': 'out_invoice', - 'reference': False, - 'account_id': sale.partner_id.property_account_receivable_id.id, - 'partner_id': sale.partner_invoice_id.id, - 'invoice_line_ids': [(0, 0, inv_line_values)], - 'currency_id': sale.pricelist_id.currency_id.id, - 'comment': sale.note, - 'payment_term_id': sale.payment_term_id.id, - 'fiscal_position_id': sale.fiscal_position_id.id or sale.partner_id.property_account_position_id.id, - 'team_id': sale.team_id.id, - } - result.append((sale.id, inv_values)) - return result - - def _create_invoices(self, cr, uid, inv_values, sale_id, context=None): - inv_obj = self.pool.get('account.invoice') - sale_obj = self.pool.get('sale.order') - inv_id = inv_obj.create(cr, uid, inv_values, context=context) - inv_obj.compute_taxes(cr, uid, [inv_id], context=context) - # add the invoice to the sales order's invoices - sale_obj.write(cr, uid, sale_id, {'invoice_ids': [(4, inv_id)]}, context=context) - return inv_id - - def create_invoices(self, cr, uid, ids, context=None): - """ create invoices for the active sales orders """ - sale_obj = self.pool.get('sale.order') - act_window = self.pool.get('ir.actions.act_window') - wizard = self.browse(cr, uid, ids[0], context) - sale_ids = context.get('active_ids', []) - if wizard.advance_payment_method == 'all': - # create the final invoices of the active sales orders - res = sale_obj.manual_invoice(cr, uid, sale_ids, context) - if context.get('open_invoices', False): - return res - return {'type': 'ir.actions.act_window_close'} - - if wizard.advance_payment_method == 'lines': - # open the list view of sales order lines to invoice - res = act_window.for_xml_id(cr, uid, 'sale', 'action_order_line_tree2', context) - res['context'] = { - 'search_default_uninvoiced': 1 - } - res['domain'] = [('order_id','=', sale_ids and sale_ids[0] or False)] - return res - assert wizard.advance_payment_method in ('fixed', 'percentage') - - inv_ids = [] - for sale_id, inv_values in self._prepare_advance_invoice_vals(cr, uid, ids, context=context): - inv_ids.append(self._create_invoices(cr, uid, inv_values, sale_id, context=context)) - - if context.get('open_invoices', False): - return self.open_invoices( cr, uid, ids, inv_ids, context=context) + advance_payment_method = fields.Selection([ + ('delivered', 'Ready to invoice'), + ('all', 'Ready to invoice (with refunds)'), + ('percentage', 'Deposit (percentage)'), + ('fixed', 'Deposit (fixed amount)') + ], string='What do you want to invoice?', default=_get_advance_payment_method, required=True) + product_id = fields.Many2one('product.product', string='Deposit Product', domain=[('type', '=', 'service')], default=_get_advance_product) + count = fields.Integer(default=_count, string='# of Orders') + amount = fields.Float('Deposit Amount', digits=dp.get_precision('Account'), help="The amount to be invoiced in advance, taxes excluded.") + + @api.onchange('advance_payment_method') + def onchange_advance_payment_method(self): + if self.advance_payment_method == 'percentage': + return {'value': {'amount':0, 'product_id':False}} + return {} + + @api.multi + def _create_invoice(self, order, so_line, amount): + inv_obj = self.env['account.invoice'] + ir_property_obj = self.env['ir.property'] + + account_id = False + if self.product_id.id: + account_id = self.product_id.property_account_income_id.id + if not account_id: + prop = ir_property_obj.get('property_account_income_categ_id', 'product.category') + prop_id = prop and prop.id or False + account_id = order.fiscal_position_id.map_account(prop_id) + if not account_id: + raise UserError( + _('There is no income account defined for this product: "%s". You may have to install a chart of account from Accounting app, settings menu.') % \ + (self.product_id.name,)) + + if self.amount <= 0.00: + raise UserError(_('The value of Advance Amount must be positive.')) + if self.advance_payment_method == 'percentage': + amount = order.amount_untaxed * self.amount / 100 + name = _("Advance of %s%%") % (self.amount,) + else: + amount = self.amount + name = _('Advance') + + invoice = inv_obj.create({ + 'name': order.client_order_ref or order.name, + 'origin': order.name, + 'type': 'out_invoice', + 'reference': False, + 'account_id': order.partner_id.property_account_receivable_id.id, + 'partner_id': order.partner_invoice_id.id, + 'invoice_line_ids': [(0, 0, { + 'name': name, + 'origin': order.name, + 'account_id': account_id, + 'price_unit': amount, + 'quantity': 1.0, + 'discount': 0.0, + 'uom_id': self.product_id.uom_id.id, + 'product_id': self.product_id.id, + 'sale_line_ids': [(6, 0, [so_line.id])], + 'invoice_line_tax_ids': [(6, 0, [x.id for x in self.product_id.taxes_id])], + 'account_analytic_id': order.project_id.id or False, + })], + 'currency_id': order.pricelist_id.currency_id.id, + 'payment_term_id': order.payment_term_id.id, + 'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id, + 'team_id': order.team_id.id, + }) + invoice.compute_taxes() + return invoice + + @api.multi + def create_invoices(self): + sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', [])) + + if self.advance_payment_method == 'delivered': + sale_orders.action_invoice_create() + elif self.advance_payment_method == 'all': + sale_orders.action_invoice_create(final=True) + else: + sale_line_obj = self.env['sale.order.line'] + for order in sale_orders: + if self.advance_payment_method == 'percentage': + amount = order.amount_untaxed * self.amount / 100 + else: + amount = self.amount + if self.product_id.invoice_policy != 'order': + raise UserError(_('The product used to invoice a deposit should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) + if self.product_id.type != 'service': + raise UserError(_("The product used to invoice an deposit should be of type 'Service'. Please use another product or update this product.")) + so_line = sale_line_obj.create({ + 'name': _('Advance: %s') % (time.strftime('%m %Y'),), + 'price_unit': amount, + 'product_uom_qty': 0.0, + 'order_id': order.id, + 'discount': 0.0, + 'product_uom': self.product_id.uom_id.id, + 'product_id': self.product_id.id, + 'tax_id': self.product_id.taxes_id, + }) + self._create_invoice(order, so_line, amount) + if self._context.get('open_invoices', False): + return sale_orders.action_view_invoice() return {'type': 'ir.actions.act_window_close'} - - def open_invoices(self, cr, uid, ids, invoice_ids, context=None): - """ open a view on one of the given invoice_ids """ - ir_model_data = self.pool.get('ir.model.data') - form_res = ir_model_data.get_object_reference(cr, uid, 'account', 'invoice_form') - form_id = form_res and form_res[1] or False - tree_res = ir_model_data.get_object_reference(cr, uid, 'account', 'invoice_tree') - tree_id = tree_res and tree_res[1] or False - - return { - 'name': _('Advance Invoice'), - 'view_type': 'form', - 'view_mode': 'form,tree', - 'res_model': 'account.invoice', - 'res_id': invoice_ids[0], - 'view_id': False, - 'views': [(form_id, 'form'), (tree_id, 'tree')], - 'context': "{'type': 'out_invoice'}", - 'type': 'ir.actions.act_window', - } diff --git a/addons/sale/wizard/sale_make_invoice_advance.xml b/addons/sale/wizard/sale_make_invoice_advance.xml index 4822fdf43d1c..cbfa1fa5cbe2 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.xml +++ b/addons/sale/wizard/sale_make_invoice_advance.xml @@ -2,23 +2,21 @@ <openerp> <data> <record id="view_sale_advance_payment_inv" model="ir.ui.view"> - <field name="name">Invoice Order</field> + <field name="name">Invoice Orders</field> <field name="model">sale.advance.payment.inv</field> <field name="arch" type="xml"> <form string="Invoice Sales Order"> <p class="oe_grey"> - Select how you want to invoice this order. This - will create a draft invoice that can be modified - before validation. + Invoices will be created in draft so that you can update + them before validation. </p> <group> + <field name="count" invisible="[('count','=',1)]" readonly="True"/> <field name="advance_payment_method" class="oe_inline" widget="radio" - on_change="onchange_method(advance_payment_method, product_id)"/> - <field name="qtty" invisible="1"/> + attrs="{'invisible': [('count','>',1)]}"/> <field name="product_id" - on_change="onchange_method(advance_payment_method, product_id)" - context="{'search_default_services': 1}" - attrs="{'invisible': [('advance_payment_method','!=','fixed')]}"/> + context="{'search_default_services': 1, 'default_type': 'service', 'default_invoice_policy': 'order'}" class="oe_inline" + attrs="{'invisible': [('advance_payment_method','not in', ('fixed','percentage'))]}"/> <label for="amount" attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"/> <div attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed','percentage'))]}"> <field name="amount" @@ -27,19 +25,12 @@ attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}" class="oe_inline"/> </div> </group> - <div> - <b><label string="After clicking 'Show Lines to Invoice', select lines to invoice and create the invoice from the 'More' dropdown menu." attrs="{'invisible': [('advance_payment_method', '!=', 'lines')]}"/></b> - </div> <footer> - <button name="create_invoices" string="Create and View Invoice" type="object" - context="{'open_invoices': True}" class="btn-primary" - attrs="{'invisible': [('advance_payment_method', '=', 'lines')]}"/> - <button name="create_invoices" string="Create Invoice" type="object" - class="btn-primary" - attrs="{'invisible': [('advance_payment_method', '=', 'lines')]}"/> - <button name="create_invoices" string="Show Lines to Invoice" type="object" - class="btn-primary" - attrs="{'invisible': [('advance_payment_method', '!=', 'lines')]}"/> + <button name="create_invoices" string="Create and View Invoices" type="object" + context="{'open_invoices': True}" class="btn-primary"/> + <button name="create_invoices" string="Create Invoices" type="object" + class="btn-primary"/> + or <button string="Cancel" class="btn-default" special="cancel"/> </footer> </form> @@ -54,5 +45,16 @@ <field name="view_mode">form</field> <field name="target">new</field> </record> + + <!-- TODO: check if we need this --> + <record model="ir.values" id="sale_order_line_make_invoice"> + <field name="model_id" ref="sale.model_sale_order_line" /> + <field name="name">Invoice Orders</field> + <field name="key2">client_action_multi</field> + <field name="value" eval="'ir.actions.act_window,' + str(ref('action_view_sale_advance_payment_inv'))" /> + <field name="key">action</field> + <field name="model">sale.order</field> + </record> + </data> </openerp> -- GitLab