diff --git a/addons/purchase/__init__.py b/addons/purchase/__init__.py index e53ec6f2f7b..c771f67af74 100644 --- a/addons/purchase/__init__.py +++ b/addons/purchase/__init__.py @@ -5,8 +5,8 @@ import mail_mail import purchase import partner import stock -import wizard import report import stock import company import res_config +import invoice diff --git a/addons/purchase/__openerp__.py b/addons/purchase/__openerp__.py index 061b60a1d80..30df961e09d 100644 --- a/addons/purchase/__openerp__.py +++ b/addons/purchase/__openerp__.py @@ -30,13 +30,11 @@ Dashboard / Reports for Purchase Management will include: 'data': [ 'security/purchase_security.xml', 'security/ir.model.access.csv', - 'purchase_workflow.xml', + 'invoice_view.xml', 'purchase_sequence.xml', 'company_view.xml', 'purchase_data.xml', 'purchase_data.yml', - 'wizard/purchase_order_group_view.xml', - 'wizard/purchase_line_invoice_view.xml', 'purchase_report.xml', 'purchase_view.xml', 'stock_view.xml', @@ -57,15 +55,9 @@ Dashboard / Reports for Purchase Management will include: 'test/fifo_returns.yml', # 'test/costmethodchange.yml', 'test/process/cancel_order.yml', - 'test/process/rfq2order2done.yml', - 'test/process/generate_invoice_from_reception.yml', - 'test/process/merge_order.yml', - 'test/process/invoice_on_poline.yml', 'test/ui/duplicate_order.yml', 'test/ui/delete_order.yml', 'test/average_price.yml', - 'test/anglo_saxon.yml', - 'test/anglo_saxon_avg_fifo.yml', ], 'demo': [ 'purchase_order_demo.yml', diff --git a/addons/purchase/company.py b/addons/purchase/company.py index 3bee858be50..c0f15294f03 100644 --- a/addons/purchase/company.py +++ b/addons/purchase/company.py @@ -1,18 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import osv,fields +from openerp import api, fields, models, _ -class company(osv.osv): +class company(models.Model): _inherit = 'res.company' - _columns = { - 'po_lead': fields.float( - 'Purchase Lead Time', required=True, - help="Margin of error for vendor lead times. When the system "\ - "generates Purchase Orders for procuring products, "\ - "they will be scheduled that many days earlier "\ - "to cope with unexpected vendor delays."), - } - _defaults = { - 'po_lead': lambda *a: 1.0, - } + po_lead = fields.Float(string='Purchase Lead Time', required=True, + help="Margin of error for vendor lead times. When the system "\ + "generates Purchase Orders for procuring products, "\ + "they will be scheduled that many days earlier "\ + "to cope with unexpected vendor delays.", default=1.0) + diff --git a/addons/purchase/invoice.py b/addons/purchase/invoice.py new file mode 100644 index 00000000000..24e0d0a815d --- /dev/null +++ b/addons/purchase/invoice.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import api, fields, models, _ +from openerp.tools.float_utils import float_compare + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + purchase_id = fields.Many2one('purchase.order', string='Purchase Order', domain=[('invoice_status', '=', 'to invoice')]) + + # Load all unsold PO lines + @api.onchange('purchase_id') + def purchase_order_change(self): + result = [] + if not self.purchase_id: + return {} + if not self.partner_id: + self.partner_id = self.purchase_id.partner_id.id + done = False + for line in self.purchase_id.order_line: + if line.product_id.purchase_method == 'purchase': + qty = line.product_qty - line.qty_invoiced + else: + qty = line.qty_received - line.qty_invoiced + if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0: + continue + done = True + account = self.env['account.invoice.line'].get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id) + taxes = line.taxes_id or line.product_id.supplier_taxes_id + invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes) + result.append({ + 'purchase_line_id': line.id, + 'name': line.name, + 'origin': self.purchase_id.origin, + 'uom_id': line.product_uom.id, + 'product_id': line.product_id.id, + 'account_id': account, + 'price_unit': line.price_unit, + 'quantity': qty, + 'discount': 0.0, + 'account_analytic_id': line.account_analytic_id.id, + 'invoice_line_tax_ids': invoice_line_tax_ids + }) + self.invoice_line_ids = result + if not done: + warning = { + 'title': _('Bill Control Warning!'), + 'message' : _('None of the products from this purchase order can be billed. You should control the purchase order.') + } + return {'warning': warning} + + +class AccountInvoiceLine(models.Model): + """ Override AccountInvoice_line to add the link to the purchase order line it is related to""" + _inherit = 'account.invoice.line' + purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', select=True, readonly=True) + + @api.model + def move_line_get(self, invoice_id): + res = super(AccountInvoiceLine, self).move_line_get(invoice_id) + invoice = self.browse(invoice_id) + if self.env.user.company_id.anglo_saxon_accounting: + if invoice.type in ['in_invoice', 'in_refund']: + for i_line in invoice.invoice_line_ids: + res.extend(self._anglo_saxon_purchase_move_lines(i_line, res)) + return res + + @api.model + def _anglo_saxon_purchase_move_lines(self, i_line, res): + """Return the additional move lines for purchase invoices and refunds. + + i_line: An account.invoice.line object. + res: The move line entries produced so far by the parent move_line_get. + """ + inv = i_line.invoice_id + company_currency = inv.company_id.currency_id + if i_line.product_id and i_line.product_id.valuation == 'real_time': + if i_line.product_id.type != 'service': + # get the price difference account at the product + acc = i_line.product_id.property_account_creditor_price_difference and i_line.product_id.property_account_creditor_price_difference.id + if not acc: + # if not found on the product get the price difference account at the category + acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ and i_line.product_id.categ_id.property_account_creditor_price_difference_categ.id + a = None + + # oa will be the stock input account + # first check the product, if empty check the category + oa = i_line.product_id.property_stock_account_input and i_line.product_id.property_stock_account_input.id + if not oa: + oa = i_line.product_id.categ_id.property_stock_account_input_categ_id and i_line.product_id.categ_id.property_stock_account_input_categ_id.id + if oa: + # get the fiscal position + fpos = i_line.invoice_id.fiscal_position_id or False + a = fpos.map_account(oa) + diff_res = [] + account_prec = inv.company_id.currency_id.decimal_places + # calculate and write down the possible price difference between invoice price and product price + for line in res: + if line.get('invl_id', 0) == i_line.id and a == line['account_id']: + uom = i_line.product_id.uom_id + valuation_price_unit = uom._compute_price(i_line.product_id.standard_price, i_line.uom_id.id) + if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id: + #for average/fifo/lifo costing method, fetch real cost price from incomming moves + stock_move_obj = self.env['stock.move'] + valuation_stock_move = stock_move_obj.search([('purchase_line_id', '=', i_line.purchase_line_id.id)], limit=1) + if valuation_stock_move: + valuation_price_unit = valuation_stock_move[0].price_unit + if inv.currency_id.id != company_currency.id: + valuation_price_unit = company_currency.compute(inv.currency_id, valuation_price_unit, context={'date': inv.date_invoice}) + if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc: + # price with discount and without tax included + price_unit = self.env['account.tax'].compute_all(line['taxes'], i_line.price_unit * (1-(i_line.discount or 0.0)/100.0),\ + inv.currency_id.id, line['quantity'])['total_excluded'] + price_line = round(valuation_price_unit * line['quantity'], account_prec) + price_diff = round(price_unit - price_line, account_prec) + line.update({'price': price_line}) + diff_res.append({ + 'type': 'src', + 'name': i_line.name[:64], + 'price_unit': round(price_diff / line['quantity'], account_prec), + 'quantity': line['quantity'], + 'price': price_diff, + 'account_id': acc, + 'product_id': line['product_id'], + 'uom_id': line['uom_id'], + 'account_analytic_id': line['account_analytic_id'], + 'taxes': line.get('taxes', []), + }) + return diff_res + return [] diff --git a/addons/purchase/invoice_view.xml b/addons/purchase/invoice_view.xml new file mode 100644 index 00000000000..4f5eba1d9eb --- /dev/null +++ b/addons/purchase/invoice_view.xml @@ -0,0 +1,68 @@ + + + Vendor Bills + account.invoice + form + tree,form,calendar,pivot,graph + + [('type','=','in_invoice')] + {'default_type':'in_invoice', 'type':'in_invoice', 'journal_type': 'purchase'} + + +

+ Click to create a draft invoice. +

+ Use this menu to control the invoices to be received from your + vendor. When registering a new bill, set the purchase order + and Odoo will fill the bill automatically according to ordered + or received quantities. +

+
+
+ + + Vendor Bills + account.invoice + form + tree,kanban,form,calendar,pivot,graph + + [('type','in',('in_invoice', 'in_refund')),('purchase_id','=',active_id)] + {'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase', 'default_purchase_id': active_id} + + +

+ Click to record a vendor bill related to this purchase. +

+

+ You can control the invoice from your vendor according to + what you purchased (services) or received (products). +

+
+
+ + + tree + + + + + + form + + + + + + account.invoice.supplier.purchase + account.invoice + + + + + + + + + + +
diff --git a/addons/purchase/partner.py b/addons/purchase/partner.py index 86aa671dd5b..9b71ff824b0 100644 --- a/addons/purchase/partner.py +++ b/addons/purchase/partner.py @@ -1,32 +1,25 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from openerp import api, fields, models -class res_partner(osv.osv): +class res_partner(models.Model): _name = 'res.partner' _inherit = 'res.partner' - def _purchase_invoice_count(self, cr, uid, ids, field_name, arg, context=None): - PurchaseOrder = self.pool['purchase.order'] - Invoice = self.pool['account.invoice'] - return { - partner_id: { - 'purchase_order_count': PurchaseOrder.search_count(cr,uid, [('partner_id', 'child_of', partner_id)], context=context), - 'supplier_invoice_count': Invoice.search_count(cr,uid, [('partner_id', 'child_of', partner_id), ('type','=','in_invoice')], context=context) - } - for partner_id in ids - } + @api.multi + def _purchase_invoice_count(self): + PurchaseOrder = self.env['purchase.order'] + Invoice = self.env['account.invoice'] + for partner in self: + partner.purchase_order_count = PurchaseOrder.search_count([('partner_id', 'child_of', partner.id)]) + partner.supplier_invoice_count = Invoice.search_count([('partner_id', 'child_of', partner.id), ('type', '=', 'in_invoice')]) - def _commercial_fields(self, cr, uid, context=None): - return super(res_partner, self)._commercial_fields(cr, uid, context=context) + @api.model + def _commercial_fields(self): + return super(res_partner, self)._commercial_fields() - _columns = { - 'property_purchase_currency_id': fields.property( - type='many2one', - relation='res.currency', - string="Supplier Currency", - help="This currency will be used, instead of the default one, for purchases from the current partner"), - 'purchase_order_count': fields.function(_purchase_invoice_count, string='# of Purchase Order', type='integer', multi="count"), - 'supplier_invoice_count': fields.function(_purchase_invoice_count, string='# Vendor Bills', type='integer', multi="count"), - } + property_purchase_currency_id = fields.Many2one('res.currency', string="Supplier Currency",\ + help="This currency will be used, instead of the default one, for purchases from the current partner") + purchase_order_count = fields.Integer(compute='_purchase_invoice_count', string='# of Purchase Order') + supplier_invoice_count = fields.Integer(compute='_purchase_invoice_count', string='# Vendor Bills') diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index b523a4593f3..453b3c7c423 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1,518 +1,213 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp import SUPERUSER_ID, workflow from datetime import datetime -import time from dateutil.relativedelta import relativedelta -from operator import attrgetter -from openerp.osv import fields, osv +from openerp import api, fields, models, _ +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.translate import _ +from openerp.tools.float_utils import float_is_zero, float_compare import openerp.addons.decimal_precision as dp -from openerp.osv.orm import browse_record_list, browse_record, browse_null -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT -from openerp.tools.float_utils import float_compare from openerp.exceptions import UserError -class purchase_order(osv.osv): - - def _amount_all(self, cr, uid, ids, field_name, arg, context=None): - res = {} - cur_obj = self.pool.get('res.currency') - line_obj = self.pool['purchase.order.line'] - 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.currency_id - for line in order.order_line: - line_price = line_obj._calc_line_base_price(cr, uid, line, - context=context) - line_qty = line_obj._calc_line_quantity(cr, uid, line, - context=context) - taxes = line.taxes_id.compute_all(line_price, cur, line_qty, - line.product_id, order.partner_id) - val1 += line.price_subtotal - for c in taxes['taxes']: - val += c.get('amount', 0.0) - 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 _set_minimum_planned_date(self, cr, uid, ids, name, value, arg, context=None): - if not value: return False - if type(ids)!=type([]): - ids=[ids] - pol_obj = self.pool.get('purchase.order.line') - for po in self.browse(cr, uid, ids, context=context): - if po.order_line: - pol_ids = pol_obj.search(cr, uid, [ - ('order_id', '=', po.id), '|', ('date_planned', '=', po.minimum_planned_date), ('date_planned', '<', value) - ], context=context) - pol_obj.write(cr, uid, pol_ids, {'date_planned': value}, context=context) - self.invalidate_cache(cr, uid, context=context) - return True - - def _minimum_planned_date(self, cr, uid, ids, field_name, arg, context=None): - res={} - purchase_obj=self.browse(cr, uid, ids, context=context) - for purchase in purchase_obj: - res[purchase.id] = False - if purchase.order_line: - min_date=purchase.order_line[0].date_planned - for line in purchase.order_line: - if line.state == 'cancel': - continue - if line.date_planned < min_date: - min_date=line.date_planned - res[purchase.id]=min_date - return res - - - def _invoiced_rate(self, cursor, user, ids, name, arg, context=None): - res = {} - for purchase in self.browse(cursor, user, ids, context=context): - tot = 0.0 - for invoice in purchase.invoice_ids: - if invoice.state not in ('draft','cancel'): - tot += invoice.amount_untaxed - if purchase.amount_untaxed: - res[purchase.id] = tot * 100.0 / purchase.amount_untaxed - else: - res[purchase.id] = 0.0 - return res - - def _shipped_rate(self, cr, uid, ids, name, arg, context=None): - if not ids: return {} - res = {} - for id in ids: - res[id] = [0.0,0.0] - cr.execute('''SELECT - p.order_id, sum(m.product_qty), m.state - FROM - stock_move m - LEFT JOIN - purchase_order_line p on (p.id=m.purchase_line_id) - WHERE - p.order_id IN %s GROUP BY m.state, p.order_id''',(tuple(ids),)) - for oid,nbr,state in cr.fetchall(): - if state=='cancel': - continue - if state=='done': - res[oid][0] += nbr or 0.0 - res[oid][1] += nbr or 0.0 - else: - res[oid][1] += nbr or 0.0 - for r in res: - if not res[r][1]: - res[r] = 0.0 - else: - res[r] = 100.0 * res[r][0] / res[r][1] - return res - - def _get_order(self, cr, uid, ids, context=None): - result = {} - for line in self.pool.get('purchase.order.line').browse(cr, uid, ids, context=context): - result[line.order_id.id] = True - return result.keys() - - def _get_purchase_order(self, cr, uid, ids, context=None): - result = {} - for order in self.browse(cr, uid, ids, context=context): - result[order.id] = True - return result.keys() - - def _invoiced(self, cursor, user, ids, name, arg, context=None): - res = {} - for purchase in self.browse(cursor, user, ids, context=context): - res[purchase.id] = all(line.invoiced for line in purchase.order_line if line.state != 'cancel') - return res - - def _get_journal(self, cr, uid, context=None): - if context is None: - context = {} - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - company_id = context.get('company_id', user.company_id.id) - journal_obj = self.pool.get('account.journal') - res = journal_obj.search(cr, uid, [('type', '=', 'purchase'), - ('company_id', '=', company_id)], - limit=1) - return res and res[0] or False - - def _get_picking_in(self, cr, uid, context=None): - obj_data = self.pool.get('ir.model.data') - type_obj = self.pool.get('stock.picking.type') - user_obj = self.pool.get('res.users') - company_id = user_obj.browse(cr, uid, uid, context=context).company_id.id - types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)], context=context) - if not types: - types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context) - if not types: - raise UserError(_("Make sure you have at least an incoming picking type defined")) - return types[0] - - def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None): - res = {} - for po_id in ids: - res[po_id] = [] - query = """ - SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po - WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id - GROUP BY picking_id, po.id - - """ - cr.execute(query, (tuple(ids), )) - picks = cr.fetchall() - for pick_id, po_id in picks: - res[po_id].append(pick_id) - return res - - def _count_all(self, cr, uid, ids, field_name, arg, context=None): - return { - purchase.id: { - 'shipment_count': len(purchase.picking_ids), - 'invoice_count': len(purchase.invoice_ids), - } - for purchase in self.browse(cr, uid, ids, context=context) - } - - STATE_SELECTION = [ - ('draft', 'Draft RFQ'), - ('sent', 'RFQ Sent'), - ('bid', 'Bid Received'), - ('confirmed', 'Waiting Approval'), - ('approved', 'Purchase Confirmed'), - ('except_picking', 'Shipping Exception'), - ('except_invoice', 'Invoice Exception'), - ('done', 'Done'), - ('cancel', 'Cancelled') - ] - - READONLY_STATES = { - 'confirmed': [('readonly', True)], - 'approved': [('readonly', True)], - 'done': [('readonly', True)] - } - - _columns = { - 'name': fields.char('Order Reference', required=True, select=True, copy=False, - help="Unique number of the purchase order, " - "computed automatically when the purchase order is created."), - 'origin': fields.char('Source Document', copy=False, - help="Reference of the document that generated this purchase order " - "request; a sales order or an internal procurement request."), - 'partner_ref': fields.char('Vendor Reference', states={'confirmed':[('readonly',True)], - 'approved':[('readonly',True)], - 'done':[('readonly',True)]}, - copy=False, - help="Reference of the sales order or bid sent by your vendor. " - "It's mainly used to do the matching when you receive the " - "products as this reference is usually written on the " - "delivery order sent by your vendor."), - 'date_order':fields.datetime('Order Date', required=True, states={'confirmed':[('readonly',True)], - 'approved':[('readonly',True)]}, - select=True, help="Depicts the date where the Quotation should be validated and converted into a Purchase Order, by default it's the creation date.", - copy=False), - 'date_approve':fields.date('Date Approved', readonly=1, select=True, copy=False, - help="Date on which purchase order has been approved"), - 'partner_id':fields.many2one('res.partner', 'Vendor', required=True, states=READONLY_STATES, - change_default=True, track_visibility='always'), - 'dest_address_id':fields.many2one('res.partner', 'Customer Address (Direct Delivery)', - states=READONLY_STATES, - help="Put an address if you want to deliver directly from the vendor to the customer. " \ - "Otherwise, keep empty to deliver to your own company." - ), - 'location_id': fields.many2one('stock.location', 'Destination', required=True, domain=[('usage','<>','view')], states=READONLY_STATES), - 'currency_id': fields.many2one('res.currency','Currency', required=True, states=READONLY_STATES), - 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, - help="The status of the purchase order or the quotation request. " - "A request for quotation is a purchase order in a 'Draft' status. " - "Then the order has to be confirmed by the user, the status switch " - "to 'Confirmed'. Then the vendor must confirm the order to change " - "the status to 'Approved'. When the purchase order is paid and " - "received, the status becomes 'Done'. If a cancel action occurs in " - "the invoice or in the receipt of goods, the status becomes " - "in exception.", - select=True, copy=False), - 'order_line': fields.one2many('purchase.order.line', 'order_id', 'Order Lines', - states={'approved':[('readonly',True)], - 'done':[('readonly',True)]}, - copy=True), - 'validator' : fields.many2one('res.users', 'Validated by', readonly=True, copy=False), - 'notes': fields.text('Terms and Conditions'), - 'invoice_ids': fields.many2many('account.invoice', 'purchase_invoice_rel', 'purchase_id', - 'invoice_id', 'Invoices', copy=False, - help="Invoices generated for a purchase order"), - 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking List', help="This is the list of receipts that have been generated for this purchase order."), - 'shipped':fields.boolean('Received', readonly=True, select=True, copy=False, - help="It indicates that a picking has been done"), - 'shipped_rate': fields.function(_shipped_rate, string='Received Ratio', type='float'), - 'invoiced': fields.function(_invoiced, string='Invoice Received', type='boolean', copy=False, - help="It indicates that an invoice has been validated"), - 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'), - 'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True, - readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)],'bid':[('readonly',False)]}, - help="Based on Purchase Order lines: place individual lines in 'Invoice Control / On Purchase Order lines' from where you can selectively create an invoice.\n" \ - "Based on generated invoice: create a draft invoice you can validate later.\n" \ - "Based on incoming shipments: let you create an invoice when receipts are validated." - ), - 'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='datetime', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.", - store = { - 'purchase.order.line': (_get_order, ['date_planned'], 10), - 'purchase.order': (_get_purchase_order, ['order_line'], 10), - } - ), - 'amount_untaxed': fields.function(_amount_all, digits=0, string='Untaxed Amount', - store={ - 'purchase.order.line': (_get_order, None, 10), - }, multi="sums", help="The amount without tax", track_visibility='always'), - 'amount_tax': fields.function(_amount_all, digits=0, string='Taxes', - store={ - 'purchase.order.line': (_get_order, None, 10), - }, multi="sums", help="The tax amount"), - 'amount_total': fields.function(_amount_all, digits=0, string='Total', - store={ - 'purchase.order.line': (_get_order, None, 10), - }, multi="sums", help="The total amount"), - 'fiscal_position_id': fields.many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position'), - 'payment_term_id': fields.many2one('account.payment.term', 'Payment Term'), - 'incoterm_id': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."), - 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'), - 'create_uid': fields.many2one('res.users', 'Responsible'), - 'company_id': fields.many2one('res.company', 'Company', required=True, select=1, states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)]}), - 'journal_id': fields.many2one('account.journal', 'Journal'), - 'bid_date': fields.date('Bid Received On', readonly=True, help="Date on which the bid was received"), - 'bid_validity': fields.date('Bid Valid Until', help="Date on which the bid expired"), - 'picking_type_id': fields.many2one('stock.picking.type', 'Deliver To', help="This will determine picking type of incoming shipment", required=True, - states={'confirmed': [('readonly', True)], 'approved': [('readonly', True)], 'done': [('readonly', True)]}), - 'related_location_id': fields.related('picking_type_id', 'default_location_dest_id', type='many2one', relation='stock.location', string="Related location", store=True), - 'related_usage': fields.related('location_id', 'usage', type='char'), - 'shipment_count': fields.function(_count_all, type='integer', string='Incoming Shipments', multi=True), - 'invoice_count': fields.function(_count_all, type='integer', string='Invoices', multi=True), - 'group_id': fields.many2one('procurement.group', string="Procurement Group"), - } - _defaults = { - 'date_order': fields.datetime.now, - 'state': 'draft', - 'name': lambda obj, cr, uid, context: '/', - 'shipped': 0, - 'invoice_method': 'order', - 'invoiced': 0, - 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'purchase.order', context=c), - 'journal_id': _get_journal, - 'currency_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id, - 'picking_type_id': _get_picking_in, - } - _sql_constraints = [ - ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), - ] +class PurchaseOrder(models.Model): _name = "purchase.order" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Purchase Order" _order = 'date_order desc, id desc' - def create(self, cr, uid, vals, context=None): - if vals.get('name', '/') == '/': - vals['name'] = self.pool.get('ir.sequence').next_by_code(cr, uid, 'purchase.order') or '/' - context = dict(context or {}, mail_create_nolog=True) - order = super(purchase_order, self).create(cr, uid, vals, context=context) - self.message_post(cr, uid, [order], body=_("RFQ created"), context=context) - return order + @api.depends('order_line.product_qty', 'order_line.price_unit', 'order_line.taxes_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.currency_id.round(amount_untaxed), + 'amount_tax': self.currency_id.round(amount_tax), + 'amount_total': amount_untaxed + amount_tax, + }) - def unlink(self, cr, uid, ids, context=None): - purchase_orders = self.read(cr, uid, ids, ['state'], context=context) - unlink_ids = [] - for s in purchase_orders: - if s['state'] in ['draft','cancel']: - unlink_ids.append(s['id']) + @api.multi + def _inverse_date_planned(self): + for order in self: + order.order_line.write({'date_planned': self.date_planned}) + + @api.depends('order_line.date_planned') + def _compute_date_planned(self): + for order in self: + min_date = False + for line in order.order_line: + if not min_date or line.date_planned < min_date: + min_date = line.date_planned + if min_date: + order.date_planned = min_date + + @api.depends('state', 'order_line.qty_invoiced', 'order_line.product_qty') + def _get_invoiced(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for order in self: + if order.state != 'purchase': + order.invoice_status = 'no' + continue + + if any(float_compare(line.qty_invoiced, line.product_qty, precision_digits=precision) == -1 for line in order.order_line): + order.invoice_status = 'to invoice' + elif all(float_compare(line.qty_invoiced, line.product_qty, precision_digits=precision) >= 0 for line in order.order_line): + order.invoice_status = 'invoiced' else: + order.invoice_status = 'no' + + @api.depends('order_line.invoice_lines.invoice_id.state') + def _compute_invoice(self): + for order in self: + invoices = self.env['account.invoice'] + for line in order.order_line: + invoices |= line.invoice_lines.mapped('invoice_id') + order.invoice_ids = invoices + order.invoice_count = len(invoices) + + @api.model + def _default_picking_type(self): + type_obj = self.env['stock.picking.type'] + company_id = self.env.user.company_id.id + types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)]) + if not types: + types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)]) + return types[0].id if types else False + + @api.depends('order_line.move_ids.picking_id') + def _compute_picking(self): + for order in self: + pickings = self.env['stock.picking'] + for line in order.order_line: + moves = line.move_ids.filtered(lambda r: r.state != 'cancel') + pickings |= moves.mapped('picking_id') + order.picking_ids = pickings + order.picking_count = len(pickings) + + READONLY_STATES = { + 'purchase': [('readonly', True)], + 'done': [('readonly', True)], + 'cancel': [('readonly', True)], + } + + name = fields.Char('Order Reference', required=True, select=True, copy=False, default='New') + origin = fields.Char('Source Document', copy=False,\ + help="Reference of the document that generated this purchase order " + "request (e.g. a sale order or an internal procurement request)") + partner_ref = fields.Char('Vendor Reference', copy=False,\ + help="Reference of the sales order or bid sent by the vendor. " + "It's used to do the matching when you receive the " + "products as this reference is usually written on the " + "delivery order sent by your vendor.") + date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Date.context_today,\ + help="Depicts the date where the Quotation should be validated and converted into a purchase order.") + date_approve = fields.Date('Approval Date', readonly=1, select=True, copy=False) + partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always') + dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\ + help="Put an address if you want to deliver directly from the vendor to the customer. "\ + "Otherwise, keep empty to deliver to your own company.") + currency_id = fields.Many2one('res.currency', 'Currency', required=True, states=READONLY_STATES,\ + default=lambda self: self.env.user.company_id.currency_id.id) + state = fields.Selection([ + ('draft', 'Draft RFQ'), + ('sent', 'RFQ Sent'), + ('to approve', 'To Approve'), + ('purchase', 'Purchase Order'), + ('done', 'Done'), + ('cancel', 'Cancelled') + ], string='Status', readonly=True, select=True, copy=False, default='draft') + order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True) + notes = fields.Text('Terms and Conditions') + + invoice_count = fields.Integer(compute="_compute_invoice", string='# of Invoices', copy=False, default=0) + invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Invoices', copy=False) + invoice_status = fields.Selection([ + ('no', 'Not purchased'), + ('to invoice', 'Waiting Invoices'), + ('invoiced', 'Invoice Received'), + ], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no') + + picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0) + picking_ids = fields.One2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False) + + date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', inverse='_inverse_date_planned', required=True, select=True, oldname='minimum_planned_date') + + 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') + amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all') + + fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position') + payment_term_id = fields.Many2one('account.payment.term', 'Payment Term') + incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") + + product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') + create_uid = fields.Many2one('res.users', 'Responsible') + company_id = fields.Many2one('res.company', 'Company', required=True, select=1, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id) + + picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ + help="This will determine picking type of incoming shipment") + group_id = fields.Many2one('procurement.group', string="Procurement Group") + + @api.model + def create(self, vals): + if vals.get('name', 'New') == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order') or '/' + return super(PurchaseOrder, self).create(vals) + + @api.multi + def unlink(self): + for order in self: + if order.state not in ['draft', 'cancel']: raise UserError(_('In order to delete a purchase order, you must cancel it first.')) + return super(PurchaseOrder, self).unlink() - # automatically sending subflow.delete upon deletion - self.signal_workflow(cr, uid, unlink_ids, 'purchase_cancel') - - return super(purchase_order, self).unlink(cr, uid, unlink_ids, context=context) - - 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 == 'approved': + @api.multi + def _track_subtype(self, init_values): + self.ensure_one() + if 'state' in init_values and self.state == 'approved': return 'purchase.mt_rfq_approved' - elif 'state' in init_values and record.state == 'confirmed': + elif 'state' in init_values and self.state == 'confirmed': return 'purchase.mt_rfq_confirmed' - elif 'state' in init_values and record.state == 'done': + elif 'state' in init_values and self.state == 'done': return 'purchase.mt_rfq_done' - return super(purchase_order, self)._track_subtype(cr, uid, ids, init_values, context=context) + return super(PurchaseOrder, self)._track_subtype(init_values) - def set_order_line_status(self, cr, uid, ids, status, context=None): - line = self.pool.get('purchase.order.line') - order_line_ids = [] - proc_obj = self.pool.get('procurement.order') - for order in self.browse(cr, uid, ids, context=context): - if status in ('draft', 'cancel'): - order_line_ids += [po_line.id for po_line in order.order_line] - else: # Do not change the status of already cancelled lines - order_line_ids += [po_line.id for po_line in order.order_line if po_line.state != 'cancel'] - if order_line_ids: - line.write(cr, uid, order_line_ids, {'state': status}, context=context) - return True - - def button_dummy(self, cr, uid, ids, context=None): - return True - - #Destination address is used when dropshipping - def onchange_dest_address_id(self, cr, uid, ids, address_id, context=None): - if not address_id: - return {} - address = self.pool.get('res.partner') - values = {} - supplier = address.browse(cr, uid, address_id, context=context) - if supplier: - location_id = supplier.property_stock_customer.id - values.update({'location_id': location_id}) - return {'value':values} - - def onchange_picking_type_id(self, cr, uid, ids, picking_type_id, context=None): - value = {} - if picking_type_id: - picktype = self.pool.get("stock.picking.type").browse(cr, uid, picking_type_id, context=context) - if picktype.default_location_dest_id: - value.update({'location_id': picktype.default_location_dest_id.id, 'related_usage': picktype.default_location_dest_id.usage}) - value.update({'related_location_id': picktype.default_location_dest_id.id}) - return {'value': value} - - def onchange_partner_id(self, cr, uid, ids, partner_id, context=None): - partner = self.pool.get('res.partner') - if not partner_id: - return {'value': { - 'fiscal_position_id': False, - 'payment_term_id': False, - 'currency_id': self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id, - }} - - company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) - if not company_id: - raise osv.except_osv(_('Error!'), _('There is no default company for the current user!')) - fp = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, context=context) - supplier = partner.browse(cr, uid, partner_id, context=context) - return {'value': { - 'fiscal_position_id': fp or supplier.property_account_position_id and supplier.property_account_position_id.id or False, - 'payment_term_id': supplier.property_supplier_payment_term_id.id or False, - 'currency_id': supplier.property_purchase_currency_id.id or self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id - }} - - def invoice_open(self, cr, uid, ids, context=None): - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - - action_id = mod_obj.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree2') - result = act_obj.read(cr, uid, action_id, context=context) - inv_ids = [] - for po in self.browse(cr, uid, ids, context=context): - inv_ids += [invoice.id for invoice in po.invoice_ids] - if not inv_ids: - raise UserError(_('Please create Invoices.')) - - if len(inv_ids) > 1: - result['domain'] = [('id', 'in', inv_ids)] + @api.onchange('partner_id') + def onchange_partner_id(self): + if not self.partner_id: + self.fiscal_position_id = False + self.payment_term_id = False + self.currency_id = False else: - res = mod_obj.xmlid_to_res_id(cr, uid, 'account.invoice_supplier_form') - result['views'] = [(res, 'form')] - result['res_id'] = inv_ids and inv_ids[0] or False - return result + self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id) + self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id + self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id + return {} - def 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. - ''' - context = dict(context or {}) - mod_obj = self.pool.get('ir.model.data') - wizard_obj = self.pool.get('purchase.order.line_invoice') - #compute the number of invoices to display - inv_ids = [] - for po in self.browse(cr, uid, ids, context=context): - if po.invoice_method == 'manual': - if not po.invoice_ids: - context.update({'active_ids' : [line.id for line in po.order_line if line.state != 'cancel']}) - wizard_obj.makeInvoices(cr, uid, [], context=context) - - for po in self.browse(cr, uid, ids, context=context): - inv_ids+= [invoice.id for invoice in po.invoice_ids] - res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_supplier_form') - res_id = res and res[1] or False - - return { - 'name': _('Vendor Bills'), - 'view_type': 'form', - 'view_mode': 'form', - 'view_id': [res_id], - 'res_model': 'account.invoice', - 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}", - 'type': 'ir.actions.act_window', - 'target': 'current', - 'res_id': inv_ids and inv_ids[0] or False, - } - - def view_picking(self, cr, uid, ids, context=None): - ''' - This function returns an action that display existing picking orders of given purchase order ids. - ''' - if context is None: - context = {} - mod_obj = self.pool.get('ir.model.data') - dummy, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')) - action = self.pool.get('ir.actions.act_window').read(cr, uid, action_id, context=context) - - pick_ids = [] - for po in self.browse(cr, uid, ids, context=context): - pick_ids += [picking.id for picking in po.picking_ids] - - #override the context to get rid of the default filtering on picking type - action['context'] = {} - #choose the view_mode accordingly - if len(pick_ids) > 1: - action['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]" - else: - res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form') - action['views'] = [(res and res[1] or False, 'form')] - action['res_id'] = pick_ids and pick_ids[0] or False - return action - - - def wkf_approve_order(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'approved', 'date_approve': fields.date.context_today(self,cr,uid,context=context)}) - return True - - def wkf_bid_received(self, cr, uid, ids, context=None): - bid_date = fields.date.context_today(self, cr, uid, context=context) - self.message_post(cr, uid, ids, body=_("Bid received on %s") % (bid_date), context=context) - return self.write(cr, uid, ids, {'state':'bid', 'bid_date': bid_date}) - - def wkf_send_rfq(self, cr, uid, ids, context=None): + @api.multi + def action_rfq_send(self): ''' This function opens a window to compose an email, with the edi purchase template message loaded by default ''' - if not context: - context= {} - ir_model_data = self.pool.get('ir.model.data') + self.ensure_one() + ir_model_data = self.env['ir.model.data'] try: - if context.get('send_rfq', False): - template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1] + if self.env.context.get('send_rfq', False): + template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase')[1] else: - template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase_done')[1] + template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase_done')[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(context) + compose_form_id = False + ctx = dict(self.env.context or {}) ctx.update({ 'default_model': 'purchase.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', @@ -529,841 +224,413 @@ class purchase_order(osv.osv): 'context': ctx, } - def print_quotation(self, cr, uid, ids, context=None): - ''' - This function prints the request for quotation 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, 'send_rfq') - return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context) + @api.multi + def print_quotation(self): + self.write({'state': "sent"}) + return self.env['report'].get_action(self, 'purchase.report_purchasequotation') - def wkf_confirm_order(self, cr, uid, ids, context=None): - todo = [] - for po in self.browse(cr, uid, ids, context=context): - if not any(line.state != 'cancel' for line in po.order_line): - raise UserError(_('You cannot confirm a purchase order without any purchase order line.')) - if po.invoice_method == 'picking' and not any([l.product_id and l.product_id.type in ('product', 'consu') and l.state != 'cancel' for l in po.order_line]): - raise osv.except_osv( - _('Error!'), - _("You cannot confirm a purchase order with Invoice Control Method 'Based on incoming shipments' that doesn't contain any stockable item.")) - for line in po.order_line: - if line.state=='draft': - todo.append(line.id) - self.pool.get('purchase.order.line').action_confirm(cr, uid, todo, context) - for id in ids: - self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid}) - return True + @api.multi + def button_approve(self): + self.write({'state': 'purchase'}) + self._action_picking_create() + return {} - def _choose_account_from_po_line(self, cr, uid, po_line, context=None): - fiscal_obj = self.pool.get('account.fiscal.position') - property_obj = self.pool.get('ir.property') - if po_line.product_id: - acc_id = po_line.product_id.property_account_expense_id.id - if not acc_id: - acc_id = po_line.product_id.categ_id.property_account_expense_categ_id.id - if not acc_id: - raise UserError(_('Define an expense account for this product: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,)) - else: - acc_id = property_obj.get(cr, uid, 'property_account_expense_categ_id', 'product.category', context=context).id - fpos = po_line.order_id.fiscal_position_id or False - #For anglo-saxon accounting - account_id = fiscal_obj.map_account(cr, uid, fpos, acc_id) - if po_line.company_id.anglo_saxon_accounting and po_line.product_id and po_line.product_id.type == 'product': - acc_id = po_line.product_id.property_stock_account_input and po_line.product_id.property_stock_account_input.id - if not acc_id: - acc_id = po_line.product_id.categ_id.property_stock_account_input_categ_id and po_line.product_id.categ_id.property_stock_account_input_categ_id.id - if acc_id: - fpos = po_line.order_id.fiscal_position_id or False - account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, acc_id) - return account_id + @api.multi + def button_draft(self): + self.write({'state': 'draft'}) + return {} - def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None): - """Collects require data from purchase order line that is used to create invoice line - for that purchase order line - :param account_id: Expense account of the product of PO line if any. - :param browse_record order_line: Purchase order line browse record - :return: Value for fields of invoice lines. - :rtype: dict - """ - return { - 'name': order_line.name, - 'account_id': account_id, - 'price_unit': order_line.price_unit or 0.0, - 'quantity': order_line.product_qty, - 'product_id': order_line.product_id.id or False, - 'uom_id': order_line.product_uom.id or False, - 'invoice_line_tax_ids': [(6, 0, [x.id for x in order_line.taxes_id])], - 'account_analytic_id': order_line.account_analytic_id.id or False, - 'purchase_line_id': order_line.id, - } + @api.multi + def button_confirm(self): + return self.button_approve() - def _prepare_invoice(self, cr, uid, order, line_ids, context=None): - """Prepare the dict of values to create the new invoice for a - purchase 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: purchase.order record to invoice - :param list(int) line_ids: list of invoice line IDs that must be - attached to the invoice - :return: dict of value to create() the invoice - """ - journal_ids = self.pool['account.journal'].search( - cr, uid, [('type', '=', 'purchase'), - ('company_id', '=', order.company_id.id)], - limit=1) - if not journal_ids: - raise UserError(_('Define purchase journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id)) - return { - 'name': order.partner_ref or order.name, - 'reference': order.partner_ref or order.name, - 'account_id': order.partner_id.property_account_payable_id.id, - 'type': 'in_invoice', - 'partner_id': order.partner_id.id, - 'currency_id': order.currency_id.id, - 'journal_id': len(journal_ids) and journal_ids[0] or False, - 'invoice_line_ids': [(6, 0, line_ids)], - 'origin': order.name, - 'fiscal_position_id': order.fiscal_position_id.id or False, - 'payment_term_id': order.payment_term_id.id or False, - 'company_id': order.company_id.id, - } - - def action_cancel_draft(self, cr, uid, ids, context=None): - if not len(ids): - return False - self.write(cr, uid, ids, {'state':'draft','shipped':0}) - self.set_order_line_status(cr, uid, ids, 'draft', context=context) - for p_id in ids: - # Deleting the existing instance of workflow for PO - self.delete_workflow(cr, uid, [p_id]) # TODO is it necessary to interleave the calls? - self.create_workflow(cr, uid, [p_id]) - return True - - def wkf_po_done(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'done'}, context=context) - self.set_order_line_status(cr, uid, ids, 'done', context=context) - - def action_invoice_create(self, cr, uid, ids, context=None): - """Generates invoice for given ids of purchase orders and links that invoice ID to purchase order. - :param ids: list of ids of purchase orders. - :return: ID of created invoice. - :rtype: int - """ - context = dict(context or {}) - - inv_obj = self.pool.get('account.invoice') - inv_line_obj = self.pool.get('account.invoice.line') - - res = False - uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - for order in self.browse(cr, uid, ids, context=context): - context.pop('force_company', None) - if order.company_id.id != uid_company_id: - #if the company of the document is different than the current user company, force the company in the context - #then re-do a browse to read the property fields for the good company. - context['force_company'] = order.company_id.id - order = self.browse(cr, uid, order.id, context=context) - - # generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line - inv_lines = [] - for po_line in order.order_line: - if po_line.state == 'cancel': - continue - acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context) - inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context) - inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context) - inv_lines.append(inv_line_id) - po_line.write({'invoice_lines': [(4, inv_line_id)]}) - - # get invoice data and create invoice - inv_data = self._prepare_invoice(cr, uid, order, inv_lines, context=context) - inv_id = inv_obj.create(cr, uid, inv_data, context=context) - - # Link this new invoice to related purchase order - order.write({'invoice_ids': [(4, inv_id)]}) - res = inv_id - return res - - def invoice_done(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'approved'}, context=context) - return True - - def has_stockable_product(self, cr, uid, ids, *args): - for order in self.browse(cr, uid, ids): - for order_line in order.order_line: - if order_line.state == 'cancel': - continue - if order_line.product_id and order_line.product_id.type in ('product', 'consu'): - return True - return False - - def wkf_action_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - self.set_order_line_status(cr, uid, ids, 'cancel', context=context) - - def action_cancel(self, cr, uid, ids, context=None): - context = context or {} - for purchase in self.browse(cr, uid, ids, context=context): - for pick in purchase.picking_ids: + @api.multi + def button_cancel(self): + for order in self: + for pick in order.picking_ids: if pick.state == 'done': - raise UserError(_('Unable to cancel the purchase order %s.') % (purchase.name) + _('You have already received some goods for it. ')) - # Check action_cancel - self.pool.get('stock.picking').action_cancel(cr, uid, [x.id for x in purchase.picking_ids if x.state != 'cancel'], context=context) - # Check procurements not related to stock move yet - if not context.get('cancel_procurement'): - cancel_procurements = [] - cancel_moves = [] - exception_procurements = [] - for line in purchase.order_line: - if line.procurement_ids: - cancel_procurements += [x.id for x in line.procurement_ids if x.state not in ('cancel', 'exception') and x.rule_id.propagate] - exception_procurements += [x.id for x in line.procurement_ids if x.state not in ('cancel', 'exception') and not x.rule_id.propagate] - cancel_moves += [x.move_dest_id.id for x in line.procurement_ids if x.move_dest_id and x.move_dest_id.state!='cancel' and x.rule_id.propagate] - if cancel_moves: - cancel_moves = list(set(cancel_moves)) - self.pool['stock.move'].action_cancel(cr, uid, cancel_moves, context=context) - if cancel_procurements: - cancel_procurements = list(set(cancel_procurements)) - self.pool['procurement.order'].write(cr, uid, cancel_procurements, {'state': 'cancel'}, context=context) - if exception_procurements: - exception_procurements = list(set(exception_procurements)) - self.pool['procurement.order'].write(cr, uid, exception_procurements, {'state': 'exception'}, context=context) - - for inv in purchase.invoice_ids: + raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name)) + for inv in order.invoice_ids: if inv and inv.state not in ('cancel', 'draft'): - raise UserError(_("Unable to cancel this purchase order.") + " " + _("You must first cancel all invoices related to this purchase order.")) - self.pool.get('account.invoice') \ - .signal_workflow(cr, uid, map(attrgetter('id'), purchase.invoice_ids), 'invoice_cancel') - self.signal_workflow(cr, uid, ids, 'purchase_cancel') - return True + raise UserError(_("Unable to cancel this purchase order.i You must first cancel related vendor bills.")) - def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, group_id, context=None): - ''' prepare the stock move data from the PO line. This function returns a list of dictionary ready to be used in stock.move's create()''' - product_uom = self.pool.get('product.uom') - price_unit = order_line.price_unit - if order_line.product_uom.id != order_line.product_id.uom_id.id: - price_unit *= order_line.product_uom.factor / order_line.product_id.uom_id.factor - if order.currency_id.id != order.company_id.currency_id.id: - #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency - price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context) - res = [] - if order.location_id.usage == 'customer': - name = order_line.product_id.with_context(dict(context or {}, lang=order.dest_address_id.lang)).name - else: - name = order_line.name or '' - move_template = { - 'name': name, - 'product_id': order_line.product_id.id, - 'product_uom': order_line.product_uom.id, - 'date': order.date_order, - 'date_expected': order_line.date_planned, - 'location_id': order.partner_id.property_stock_supplier.id, - 'location_dest_id': order.location_id.id, - 'picking_id': picking_id, - 'partner_id': order.dest_address_id.id, - 'move_dest_id': False, - 'state': 'draft', - 'purchase_line_id': order_line.id, - 'company_id': order.company_id.id, - 'price_unit': price_unit, - 'picking_type_id': order.picking_type_id.id, - 'group_id': group_id, - 'procurement_id': False, - 'origin': order.name, - 'route_ids': order.picking_type_id.warehouse_id and [(6, 0, [x.id for x in order.picking_type_id.warehouse_id.route_ids])] or [], - 'warehouse_id':order.picking_type_id.warehouse_id.id, - } + if not self.env.context.get('cancel_procurement'): + procurements = order.order_line.mapped('procurement_ids') + procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and r.rule_id.propagate).write({'state': 'cancel'}) + procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and not r.rule_id.propagate).write({'state': 'exception'}) + moves = procurements.filtered(lambda r: r.rule_id.propagate).mapped('move_dest_id') + moves.filtered(lambda r: r.state != 'cancel').action_cancel() - diff_quantity = order_line.product_qty - for procurement in order_line.procurement_ids: - procurement_qty = product_uom._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, to_uom_id=order_line.product_uom.id) - tmp = move_template.copy() - tmp.update({ - 'product_uom_qty': min(procurement_qty, diff_quantity), - 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination - 'procurement_id': procurement.id, - 'propagate': procurement.rule_id.propagate, + self.write({'state': 'cancel'}) + + @api.multi + def button_done(self): + self.write({'state': 'done'}) + + @api.multi + def _get_destination_location(self): + self.ensure_one() + if self.dest_address_id: + return self.dest_address_id.property_stock_customer.id + return self.picking_type_id.default_location_dest_id.id + + @api.model + def _create_picking(self): + if not self.group_id: + self.group_id = self.group_id.create({ + 'name': self.name, + 'partner_id': self.partner_id.id }) - diff_quantity -= min(procurement_qty, diff_quantity) - res.append(tmp) - #if the order line has a bigger quantity than the procurement it was for (manually changed or minimal quantity), then - #split the future stock move in two because the route followed may be different. - if float_compare(diff_quantity, 0.0, precision_rounding=order_line.product_uom.rounding) > 0: - move_template['product_uom_qty'] = diff_quantity - res.append(move_template) - return res + return self.env['stock.picking'].create({ + 'picking_type_id': self.picking_type_id.id, + 'partner_id': self.partner_id.id, + 'date': self.date_order, + 'origin': self.name, + 'location_dest_id': self._get_destination_location(), + 'location_id': self.partner_id.property_stock_supplier.id + }) - def _create_stock_moves(self, cr, uid, order, order_lines, picking_id=False, context=None): - """Creates appropriate stock moves for given order lines, whose can optionally create a - picking if none is given or no suitable is found, then confirms the moves, makes them - available, and confirms the pickings. - - If ``picking_id`` is provided, the stock moves will be added to it, otherwise a standard - incoming picking will be created to wrap the stock moves (default behavior of the stock.move) - - Modules that wish to customize the procurements or partition the stock moves over - multiple stock pickings may override this method and call ``super()`` with - different subsets of ``order_lines`` and/or preset ``picking_id`` values. - - :param browse_record order: purchase order to which the order lines belong - :param list(browse_record) order_lines: purchase order line records for which picking - and moves should be created. - :param int picking_id: optional ID of a stock picking to which the created stock moves - will be added. A new picking will be created if omitted. - :return: None - """ - stock_move = self.pool.get('stock.move') - todo_moves = [] - if order.group_id: - new_group = order.group_id.id - else: - new_group = self.pool.get("procurement.group").create(cr, uid, {'name': order.name, 'partner_id': order.partner_id.id}, context=context) - - for order_line in order_lines: - if order_line.state == 'cancel': - continue - if not order_line.product_id: - continue - - if order_line.product_id.type in ('product', 'consu'): - for vals in self._prepare_order_line_move(cr, uid, order, order_line, picking_id, new_group, context=context): - move = stock_move.create(cr, uid, vals, context=context) - todo_moves.append(move) - - todo_moves = stock_move.action_confirm(cr, uid, todo_moves) - stock_move.force_assign(cr, uid, todo_moves) - - def test_moves_done(self, cr, uid, ids, context=None): - '''PO is done at the delivery side if all the incoming shipments are done''' - for purchase in self.browse(cr, uid, ids, context=context): - for picking in purchase.picking_ids: - if picking.state != 'done': - return False + @api.multi + def _action_picking_create(self): + for order in self: + ptypes = order.order_line.mapped('product_id.type') + if ('product' in ptypes) or ('consu' in ptypes): + picking = order._create_picking() + moves = order.order_line._create_stock_moves(picking) + moves.action_confirm() + moves.force_assign() return True - def test_moves_except(self, cr, uid, ids, context=None): - ''' PO is in exception at the delivery side if one of the picking is canceled - and the other pickings are completed (done or canceled) - ''' - at_least_one_canceled = False - alldoneorcancel = True - for purchase in self.browse(cr, uid, ids, context=context): - for picking in purchase.picking_ids: - if picking.state == 'cancel': - at_least_one_canceled = True - if picking.state not in ['done', 'cancel']: - alldoneorcancel = False - return at_least_one_canceled and alldoneorcancel - def move_lines_get(self, cr, uid, ids, *args): - res = [] - for order in self.browse(cr, uid, ids, context={}): - for line in order.order_line: - res += [x.id for x in line.move_ids] - return res - - def action_picking_create(self, cr, uid, ids, context=None): - for order in self.browse(cr, uid, ids): - picking_vals = { - 'picking_type_id': order.picking_type_id.id, - 'partner_id': order.partner_id.id, - 'date': order.date_order, - 'origin': order.name, - 'location_id': order.partner_id.property_stock_supplier.id, - 'location_dest_id': order.location_id.id, - } - picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context) - self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context) - return picking_id - - def picking_done(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context) - # Do check on related procurements: - proc_obj = self.pool.get("procurement.order") - po_lines = [] - for po in self.browse(cr, uid, ids, context=context): - po_lines += [x.id for x in po.order_line if x.state != 'cancel'] - if po_lines: - procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', po_lines)], context=context) - if procs: - proc_obj.check(cr, uid, procs, context=context) - for id in ids: - self.message_post(cr, uid, id, body=_("Products received"), context=context) - return True - - def do_merge(self, cr, uid, ids, context=None): - """ - To merge similar type of purchase orders. - Orders will only be merged if: - * Purchase Orders are in draft - * Purchase Orders belong to the same partner - * Purchase Orders are have same stock location, same pricelist, same currency - Lines will only be merged if: - * Order lines are exactly the same except for the quantity and unit - - @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: new purchase order id - - """ - #TOFIX: merged order line should be unlink - def make_key(br, fields): - list_key = [] - for field in fields: - field_val = getattr(br, field) - if field in ('product_id', 'account_analytic_id'): - if not field_val: - field_val = False - if isinstance(field_val, browse_record): - field_val = field_val.id - elif isinstance(field_val, browse_null): - field_val = False - elif isinstance(field_val, browse_record_list): - field_val = ((6, 0, tuple([v.id for v in field_val])),) - list_key.append((field, field_val)) - list_key.sort() - return tuple(list_key) - - context = dict(context or {}) - - # Compute what the new orders should contain - new_orders = {} - - order_lines_to_move = {} - for porder in [order for order in self.browse(cr, uid, ids, context=context) if order.state == 'draft']: - order_key = make_key(porder, ('partner_id', 'location_id', 'currency_id')) - new_order = new_orders.setdefault(order_key, ({}, [])) - new_order[1].append(porder.id) - order_infos = new_order[0] - order_lines_to_move.setdefault(order_key, []) - - if not order_infos: - order_infos.update({ - 'origin': porder.origin, - 'date_order': porder.date_order, - 'partner_id': porder.partner_id.id, - 'dest_address_id': porder.dest_address_id.id, - 'picking_type_id': porder.picking_type_id.id, - 'location_id': porder.location_id.id, - 'currency_id': porder.currency_id.id, - 'state': 'draft', - 'order_line': {}, - 'notes': '%s' % (porder.notes or '',), - 'fiscal_position_id': porder.fiscal_position_id and porder.fiscal_position_id.id or False, - }) - else: - if porder.date_order < order_infos['date_order']: - order_infos['date_order'] = porder.date_order - if porder.notes: - order_infos['notes'] = (order_infos['notes'] or '') + ('\n%s' % (porder.notes,)) - if porder.origin: - order_infos['origin'] = (order_infos['origin'] or '') + ' ' + porder.origin - - order_lines_to_move[order_key] += [order_line.id for order_line in porder.order_line - if order_line.state != 'cancel'] - - allorders = [] - orders_info = {} - for order_key, (order_data, old_ids) in new_orders.iteritems(): - # skip merges with only one order - if len(old_ids) < 2: - allorders += (old_ids or []) - continue - - # cleanup order line data - for key, value in order_data['order_line'].iteritems(): - del value['uom_factor'] - value.update(dict(key)) - order_data['order_line'] = [(6, 0, order_lines_to_move[order_key])] - - # create the new order - context.update({'mail_create_nolog': True}) - neworder_id = self.create(cr, uid, order_data) - self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context) - orders_info.update({neworder_id: old_ids}) - allorders.append(neworder_id) - - # make triggers pointing to the old orders point to the new order - for old_id in old_ids: - self.redirect_workflow(cr, uid, [(old_id, neworder_id)]) - self.signal_workflow(cr, uid, [old_id], 'purchase_cancel') - - return orders_info - - def _set_po_lines_invoiced(self, cr, uid, ids, context=None): - for po in self.browse(cr, uid, ids, context=context): - is_invoiced = [] - if po.invoice_method == 'picking': - # We determine the invoiced state of the PO line based on the invoiced state - # of the associated moves. This should cover all possible cases: - # - all moves are done and invoiced - # - a PO line is split into multiple moves (e.g. if multiple pickings): some - # pickings are done, some are in progress, some are cancelled - for po_line in po.order_line: - if (po_line.move_ids and - all(move.state in ('done', 'cancel') for move in po_line.move_ids) and - not all(move.state == 'cancel' for move in po_line.move_ids) and - all(move.invoice_state == 'invoiced' for move in po_line.move_ids if move.state == 'done')): - is_invoiced.append(po_line.id) - else: - for po_line in po.order_line: - if (po_line.invoice_lines and - all(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines)): - is_invoiced.append(po_line.id) - if is_invoiced: - self.pool['purchase.order.line'].write(cr, uid, is_invoiced, {'invoiced': True}) - workflow.trg_write(uid, 'purchase.order', po.id, cr) - - -class purchase_order_line(osv.osv): - def _calc_line_base_price(self, cr, uid, line, context=None): - """Return the base price of the line to be used for tax calculation. - - This function can be extended by other modules to modify this base - price (adding a discount, for example). - """ - return line.price_unit - - def _calc_line_quantity(self, cr, uid, line, context=None): - """Return the base quantity of the line to be used for the subtotal. - - This function can be extended by other modules to modify this base - quantity (adding for example offers 3x2 and so on). - """ - return line.product_qty - - def _amount_line(self, cr, uid, ids, prop, arg, context=None): - res = {} - for line in self.browse(cr, uid, ids, context=context): - line_price = self._calc_line_base_price(cr, uid, line, - context=context) - line_qty = self._calc_line_quantity(cr, uid, line, - context=context) - cur = line.order_id.currency_id - taxes = line.taxes_id.compute_all(line_price, cur, - line_qty, line.product_id, - line.order_id.partner_id) - - res[line.id] = taxes['total_excluded'] - return res - - def _get_uom_id(self, cr, uid, context=None): - 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 - - _columns = { - 'name': fields.text('Description', required=True), - 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True), - 'date_planned': fields.datetime('Scheduled Date', required=True, select=True), - 'taxes_id': fields.many2many('account.tax', 'purchase_order_taxe', 'ord_id', 'tax_id', 'Taxes'), - 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True), - 'product_id': fields.many2one('product.product', 'Product', domain=[('purchase_ok','=',True)], change_default=True), - 'move_ids': fields.one2many('stock.move', 'purchase_line_id', 'Reservation', readonly=True, ondelete='set null'), - 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price')), - 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0), - 'order_id': fields.many2one('purchase.order', 'Order Reference', select=True, required=True, ondelete='cascade'), - 'account_analytic_id':fields.many2one('account.analytic.account', 'Analytic Account',), - 'company_id': fields.related('order_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True), - 'state': fields.selection([('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], - 'Status', required=True, readonly=True, copy=False, - help=' * The \'Draft\' status is set automatically when purchase order in draft status. \ - \n* The \'Confirmed\' status is set automatically as confirm when purchase order in confirm status. \ - \n* The \'Done\' status is set automatically when purchase order is set as done. \ - \n* The \'Cancelled\' status is set automatically when user cancel purchase order.'), - 'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', - 'order_line_id', 'invoice_id', 'Invoice Lines', - readonly=True, copy=False), - 'invoiced': fields.boolean('Invoiced', readonly=True, copy=False), - 'partner_id': fields.related('order_id', 'partner_id', string='Partner', readonly=True, type="many2one", relation="res.partner", store=True), - 'date_order': fields.related('order_id', 'date_order', string='Order Date', readonly=True, type="datetime"), - 'procurement_ids': fields.one2many('procurement.order', 'purchase_line_id', string='Associated procurements'), - } - _defaults = { - 'product_uom' : _get_uom_id, - 'product_qty': lambda *a: 1.0, - 'state': lambda *args: 'draft', - 'invoiced': lambda *a: 0, - } - _table = 'purchase_order_line' +class PurchaseOrderLine(models.Model): _name = 'purchase.order.line' _description = 'Purchase Order Line' - def unlink(self, cr, uid, ids, context=None): - for line in self.browse(cr, uid, ids, context=context): - if line.order_id.state in ['approved', 'done'] and line.state not in ['draft', 'cancel']: + @api.depends('product_qty', 'price_unit', 'taxes_id') + def _compute_amount(self): + for line in self: + taxes = line.taxes_id.compute_all(line.price_unit, line.order_id.currency_id, line.product_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'], + }) + + @api.depends('invoice_lines.invoice_id.state') + def _compute_qty_invoiced(self): + for line in self: + qty = 0.0 + for inv_line in line.invoice_lines: + qty += inv_line.uom_id._compute_qty_obj(inv_line.uom_id, inv_line.quantity, line.product_uom) + line.qty_invoiced = qty + + @api.depends('move_ids.state') + def _compute_qty_received(self): + for line in self: + if line.order_id.state not in ['purchase', 'done']: + line.qty_received = 0.0 + continue + if line.product_id.type not in ['consu', 'product']: + line.qty_received = line.product_qty + continue + total = 0.0 + for move in line.move_ids: + if move.state == 'done': + total += move.product_qty + line.qty_received = total + + name = fields.Text(string='Description', required=True) + product_qty = fields.Float(string='Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, default=0.0) + date_planned = fields.Datetime(string='Scheduled Date', required=True, select=True) + taxes_id = fields.Many2many('account.tax', string='Taxes') + product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True) + product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True) + move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False) + price_unit = fields.Float(string='Unit Price', required=True, digits_compute=dp.get_precision('Product Price')) + + price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) + price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) + price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True) + + order_id = fields.Many2one('purchase.order', string='Order Reference', select=True, required=True, ondelete='cascade') + account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') + company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True) + + invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Invoice Lines", readonly=True, copy=False) + + # Replace by invoiced Qty + qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", store=True) + qty_received = fields.Float(compute='_compute_qty_received', string="Received Qty", store=True) + + partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True) + currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) + date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True) + procurement_ids = fields.One2many('procurement.order', 'purchase_line_id', string='Associated Procurements', copy=False) + + @api.multi + def _create_stock_moves(self, picking): + moves = self.env['stock.move'] + done = self.env['stock.move'].browse() + for line in self: + order = line.order_id + price_unit = line.price_unit + if line.product_uom.id != line.product_id.uom_id.id: + price_unit *= line.product_uom.factor / line.product_id.uom_id.factor + if order.currency_id != order.company_id.currency_id: + price_unit = order.currency_id.compute(price_unit, order.company_id.currency_id, round=False) + + template = { + 'name': line.name or '', + 'product_id': line.product_id.id, + 'product_uom': line.product_uom.id, + 'date': line.order_id.date_order, + 'date_expected': line.date_planned, + 'location_id': line.order_id.partner_id.property_stock_supplier.id, + 'location_dest_id': line.order_id._get_destination_location(), + 'picking_id': picking.id, + 'partner_id': line.order_id.dest_address_id.id, + 'move_dest_id': False, + 'state': 'draft', + 'purchase_line_id': line.id, + 'company_id': line.order_id.company_id.id, + 'price_unit': price_unit, + 'picking_type_id': line.order_id.picking_type_id.id, + 'group_id': line.order_id.group_id.id, + 'procurement_id': False, + 'origin': line.order_id.name, + 'route_ids': line.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in line.order_id.picking_type_id.warehouse_id.route_ids])] or [], + 'warehouse_id':line.order_id.picking_type_id.warehouse_id.id, + } + + # Fullfill all related procurements with this po line + diff_quantity = line.product_qty + for procurement in line.procurement_ids: + procurement_qty = procurement.product_uom._compute_qty_obj(procurement.product_uom, procurement.product_qty, line.product_uom) + tmp = template.copy() + tmp.update({ + 'product_uom_qty': min(procurement_qty, diff_quantity), + 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination + 'procurement_id': procurement.id, + 'propagate': procurement.rule_id.propagate, + }) + done += moves.create(tmp) + diff_quantity -= min(procurement_qty, diff_quantity) + if float_compare(diff_quantity, 0.0, precision_rounding=line.product_uom.rounding) > 0: + template['product_uom_qty'] = diff_quantity + done += moves.create(template) + return done + + @api.multi + def unlink(self): + for line in self: + if line.order_id.state in ['approved', 'done']: raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,)) - procurement_obj = self.pool.get('procurement.order') - procurement_ids_to_except = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context) - if procurement_ids_to_except: - for po_id in procurement_ids_to_except: - procurement_obj.message_post(cr, uid, po_id, body=_('Purchase order line deleted.'), context=context) - procurement_obj.write(cr, uid, procurement_ids_to_except, {'state': 'exception'}, context=context) - return super(purchase_order_line, self).unlink(cr, uid, ids, context=context) + line.procurement_ids.message_post(body=_('Purchase order line deleted.')) + line.procurement_ids.write({'state': 'exception'}) + return super(PurchaseOrderLine, self).unlink() - def onchange_product_uom(self, cr, uid, ids, product_id, qty, uom_id, - partner_id, date_order=False, fiscal_position_id=False, date_planned=False, - name=False, price_unit=False, state='draft', currency_id=False, context=None): - """ - onchange handler of product_uom. - """ - if context is None: - context = {} - if not uom_id: - return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}} - context = dict(context, purchase_uom_check=True) - return self.onchange_product_id(cr, uid, ids, product_id, qty, uom_id, - partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned, - name=name, price_unit=price_unit, state=state, replace=False, currency_id=currency_id, context=context) - - def _get_date_planned(self, cr, uid, product, date_order_str, context=None): + @api.model + def _get_date_planned(self, product, po=False): """Return the datetime value to use as Schedule Date (``date_planned``) for - PO Lines that correspond to the given product.supplierinfo, + PO Lines that correspond to the given product.seller_ids, when ordered at `date_order_str`. :param browse_record | False product: product.product, used to determine delivery delay thanks to the selected seller field (if False, default delay = 0) - :param str date_order_str: date of order field, as a string in - DEFAULT_SERVER_DATETIME_FORMAT + :param browse_record | False po: purchase.order, necessary only if + the PO line is not yet attached to a PO. :rtype: datetime :return: desired Schedule Date for the PO line """ supplier_delay = int(product.seller_delay) if product else 0 - return datetime.strptime(date_order_str, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay) + if po: + date_order = po.date_order + else: + date_order = self.order_id.date_order + if date_order: + return datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=supplier_delay) + else: + return datetime.today() + relativedelta(days=supplier_delay) - def action_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - # We will group by PO first, so we do the check only once for each PO - purchase_orders = list(set([x.order_id for x in self.browse(cr, uid, ids, context=context)])) - for purchase in purchase_orders: - if all([l.state == 'cancel' for l in purchase.order_line]): - self.pool.get('purchase.order').action_cancel(cr, uid, [purchase.id], context=context) + @api.onchange('product_id', 'product_qty', 'product_uom') + def onchange_product_id(self): + result = {} + if not self.product_id: + return {} - def _check_product_uom_group(self, cr, uid, context=None): - group_uom = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'group_uom') - res = [user for user in group_uom.users if user.id == uid] - return len(res) and True or False + if self.product_id.uom_id.category_id.id != self.product_uom.category_id.id: + self.product_uom = self.product_id.uom_po_id + result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]} - def onchange_product_id(self, cr, uid, ids, product_id, qty, uom_id, - partner_id, date_order=False, fiscal_position_id=False, date_planned=False, - name=False, price_unit=False, state='draft', replace=True, currency_id=False, context=None): + res = self._get_name_price_quantity_date( + self.product_id, + self.partner_id, + datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT) if self.order_id.date_order else datetime.today(), + self.product_qty, + self.product_uom, + self.order_id.currency_id) + + self.name = res['name'] + self.product_qty = res['quantity'] if not res['quantity'] else max(self.product_qty, res['quantity']) + self.price_unit = res['price_unit'] + self.date_planned = res['date_planned'] + + taxes = self.product_id.supplier_taxes_id + fpos = self.order_id.fiscal_position_id + if fpos: + self.taxes_id = fpos.map_tax(taxes) + + result['value'] = { + 'name': self.name, + 'product_uom': self.product_uom.id, + 'product_qty': self.product_qty, + 'date_planned': self.date_planned, + 'taxes_id': self.taxes_id.ids, + } + + return result + + @api.model + def _get_name_price_quantity_date(self, product_id, partner_id, date, quantity, product_uom, currency_id, order_id=False): + """Return the name of the product, the seller quantity, the unit price and + the date planned of a given product. + + :param browse_record product_id: product.product, for which we want to + get the information + :param str lang: language in which we want the product name + :param browse_record partner_id: res.partner, supplier of the product + :param str date: order date + :param float quantity: quantity of product in product_uom UoM + :param browse_record product_uom: product.uom, product_uom UoM in which is + expressed the quantity + :param browse_record currency_id: res.currency, currency in which we want the price + :param browse_record | False order_id: purchase.order, purchase order in case we + try to get the data for a PO line not yet attached to a PO. + :rtype: dict + :return: name, seller quantity, seller price, date planned """ - onchange handler of product_id. - """ - if context is None: - context = {} - res = {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}} - if not product_id: - if not uom_id: - uom_id = self.default_get(cr, uid, ['product_uom'], context=context).get('product_uom', False) - res['value']['product_uom'] = uom_id - return res - product_product = self.pool.get('product.product') - product_uom = self.pool.get('product.uom') - res_partner = self.pool.get('res.partner') - account_fiscal_position = self.pool.get('account.fiscal.position') - account_tax = self.pool.get('account.tax') + # Switch quantity in uom_id + quantity_uom_id = quantity + if quantity_uom_id and product_id.uom_id != product_uom: + quantity_uom_id = product_uom._compute_qty_obj(product_uom, quantity_uom_id, product_id.uom_id) - # - check for the presence of partner_id and pricelist_id - #if not partner_id: - # raise UserError(_('Select a partner in purchase order to choose a product.')) - #if not pricelist_id: - # raise UserError(_('Select a price list in the purchase order form before choosing a product.')) + product = product_id.with_context({ + 'lang': partner_id.lang, + 'partner_id': partner_id.id, + 'date': date, + 'quantity': quantity_uom_id, + }) - # - determine name and notes based on product in partner lang. - context_partner = context.copy() - context_partner.update({'partner': partner_id, 'quantity': qty}) - if partner_id: - lang = res_partner.browse(cr, uid, partner_id).lang - context_partner.update({'lang': lang, 'partner_id': partner_id}) - product = product_product.browse(cr, uid, product_id, context=context_partner) - price = product.seller_price - if product.selected_seller_id.currency_id.id != currency_id: - price = self.pool['res.currency'].compute(cr, uid, product.selected_seller_id.currency_id.id, currency_id, price, context=dict(context or {}, date=date_order)) - if replace: - #call name_get() with partner in the context to eventually match name and description in the seller_ids field - dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0] - if product.description_purchase: - name += '\n' + product.description_purchase - res['value'].update({'name': name}) + # Switch quantity back in product_uom + quantity = max(quantity_uom_id, product.seller_qty) + if quantity and product_id.uom_id != product_uom: + quantity = product_id.uom_id._compute_qty_obj(product_id.uom_id, quantity, product_uom) - # - set a domain on product_uom - res['domain'] = {'product_uom': [('category_id','=',product.uom_id.category_id.id)]} + price_unit = product.seller_price + if price_unit and partner_id.currency_id != currency_id: + price_unit = partner_id.currency_id.compute(price_unit, currency_id) - # - check that uom and product uom belong to the same category - product_uom_po_id = product.uom_po_id.id - if not uom_id: - uom_id = product_uom_po_id + date_planned = self._get_date_planned(product, po=order_id).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - uom = product_uom.browse(cr, uid, uom_id, context=context) - if product.uom_id.category_id.id != uom.category_id.id: - if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context): - res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')} - uom_id = product_uom_po_id + name = product.display_name + if product.description_purchase: + name += '\n' + product.description_purchase - res['value'].update({'product_uom': uom_id}) - - # - determine product_qty and date_planned based on seller info - if not date_order: - date_order = fields.datetime.now() - dt = self._get_date_planned(cr, uid, product, date_order, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - qty = qty or 1.0 - res['value'].update({'date_planned': date_planned or dt}) - if qty: - res['value'].update({'product_qty': qty}) - - taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id)) - fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False - taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes) - price = self.pool['account.tax']._fix_tax_included_price(cr, uid, price, product.supplier_taxes_id, taxes_ids) - res['value'].update({'price_unit': price, 'taxes_id': taxes_ids}) - return res - - product_id_change = onchange_product_id - product_uom_change = onchange_product_uom - - def action_confirm(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'confirmed'}, context=context) - return True + return { + 'name': name, + 'quantity': quantity, + 'price_unit': price_unit, + 'date_planned': date_planned, + } -class procurement_rule(osv.osv): +class ProcurementRule(models.Model): _inherit = 'procurement.rule' - - def _get_action(self, cr, uid, context=None): - return [('buy', _('Buy'))] + super(procurement_rule, self)._get_action(cr, uid, context=context) + @api.model + def _get_action(self): + return [('buy', _('Buy'))] + super(ProcurementRule, self)._get_action() -class procurement_order(osv.osv): +class ProcurementOrder(models.Model): _inherit = 'procurement.order' - _columns = { - 'purchase_line_id': fields.many2one('purchase.order.line', 'Purchase Order Line'), - 'purchase_id': fields.related('purchase_line_id', 'order_id', type='many2one', relation='purchase.order', string='Purchase Order'), - } - def propagate_cancels(self, cr, uid, ids, context=None): - purchase_line_obj = self.pool.get('purchase.order.line') - lines_to_cancel = [] - for procurement in self.browse(cr, uid, ids, context=context): + purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Order Line') + purchase_id = fields.Many2one(related='purchase_line_id.order_id', string='Purchase Order') + + @api.multi + def propagate_cancels(self): + result = super(ProcurementOrder, self).propagate_cancels() + for procurement in self: if procurement.rule_id.action == 'buy' and procurement.purchase_line_id: - if procurement.purchase_line_id.state not in ('draft', 'cancel'): + if procurement.purchase_line_id.order_id.state not in ('draft', 'cancel', 'sent', 'to validate'): raise UserError( - _('Can not cancel this procurement like this as the related purchase order has been confirmed already. Please cancel the purchase order first. ')) + _('Can not cancel a procurement related to a purchase order. Please cancel the purchase order first.')) + if procurement.purchase_line_id: + price_unit = 0.0 + product_qty = 0.0 + others_procs = procurement.purchase_line_id.procurement_ids.filtered(lambda r: r != procurement) + for other_proc in others_procs: + if other_proc.state not in ['cancel', 'draft']: + product_qty += other_proc.product_uom._compute_qty_obj(other_proc.product_uom, other_proc.product_qty, procurement.purchase_line_id.product_uom) - new_qty, new_price = self._calc_new_qty_price(cr, uid, procurement, cancel=True, context=context) - if new_qty != procurement.purchase_line_id.product_qty: - purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': new_qty, 'price_unit': new_price}, context=context) - if float_compare(new_qty, 0.0, precision_rounding=procurement.product_uom.rounding) != 1: - if procurement.purchase_line_id.id not in lines_to_cancel: - lines_to_cancel += [procurement.purchase_line_id.id] - if lines_to_cancel: - purchase_line_obj.action_cancel(cr, uid, lines_to_cancel, context=context) - purchase_line_obj.unlink(cr, uid, lines_to_cancel, context=context) - return super(procurement_order, self).propagate_cancels(cr, uid, ids, context=context) + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + if not float_is_zero(product_qty, precision_digits=precision): + res = self.env['purchase.order.line']._get_name_price_quantity_date( + procurement.product_id, + procurement.purchase_line_id.partner_id, + datetime.strptime(procurement.purchase_line_id.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT), + product_qty, + procurement.product_uom, + procurement.purchase_line_id.order_id.currency_id) - def _run(self, cr, uid, procurement, context=None): + product_qty = res['quantity'] + price_unit = res['price_unit'] + + procurement.purchase_line_id.product_qty = product_qty + procurement.purchase_line_id.price_unit = price_unit + + return result + + @api.model + def _run(self, procurement): if procurement.rule_id and procurement.rule_id.action == 'buy': - #make a purchase order for the procurement - return self.make_po(cr, uid, [procurement.id], context=context)[procurement.id] - return super(procurement_order, self)._run(cr, uid, procurement, context=context) + return procurement.make_po() + return super(ProcurementOrder, self)._run(procurement) - #TODO: Autocommit needed? - def run(self, cr, uid, ids, autocommit=False, context=None): - procs = self.browse(cr, uid, ids, context=context) - to_assign = [x for x in procs if x.state not in ('running', 'done')] - self._assign_multi(cr, uid, to_assign, context=context) - buy_ids = [x.id for x in to_assign if x.rule_id and x.rule_id.action == 'buy'] - if buy_ids: - result_dict = self.make_po(cr, uid, buy_ids, context=context) - runnings = [] - exceptions = [] - for proc in result_dict.keys(): - if result_dict[proc]: - runnings += [proc] - else: - exceptions += [proc] - if runnings: - self.write(cr, uid, runnings, {'state': 'running'}, context=context) - if exceptions: - self.write(cr, uid, exceptions, {'state': 'exception'}, context=context) - set_others = set(ids) - set(buy_ids) - return super(procurement_order, self).run(cr, uid, list(set_others), context=context) - - def _check(self, cr, uid, procurement, context=None): + @api.model + def _check(self, procurement): if procurement.purchase_line_id: - if procurement.purchase_line_id.order_id.shipped: - return True - elif procurement.move_ids: - moves = self.pool.get('stock.move').browse(cr, uid, [x.id for x in procurement.move_ids], context=context) - return all(move.state == 'done' for move in moves) - return super(procurement_order, self)._check(cr, uid, procurement, context=context) - - def _check_supplier_info(self, cr, uid, ids, context=None): - ''' Check the vendor info field of a product and write an error message on the procurement if needed. - Returns True if all needed information is there, False if some configuration mistake is detected. - ''' - partner_obj = self.pool.get('res.partner') - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - for procurement in self.browse(cr, uid, ids, context=context): - message = '' - partner = procurement.product_id.seller_id #Taken Main Vendor of Product of Procurement. - - if not procurement.product_id.seller_ids: - message = _('No vendor defined for this product !') - elif not partner: - message = _('No default vendor defined for this product') - elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']: - message = _('No address defined for the vendor') - - if message: - if procurement.message != message: - cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id)) + if not procurement.move_ids: return False + return all(move.state == 'done' for move in procurement.move_ids) + return super(ProcurementOrder, self)._check(procurement) - if user.company_id and user.company_id.partner_id: - if partner.id == user.company_id.partner_id.id: - raise UserError(_('The product "%s" has been defined with your company as reseller which seems to be a configuration error!' % procurement.product_id.name)) + @api.v8 + def _get_purchase_schedule_date(self): + procurement_date_planned = datetime.strptime(self.date_planned, DEFAULT_SERVER_DATETIME_FORMAT) + schedule_date = (procurement_date_planned - relativedelta(days=self.company_id.po_lead)) + return schedule_date - return True - - def create_procurement_purchase_order(self, cr, uid, procurement, po_vals, line_vals, context=None): - """Create the purchase order from the procurement, using - the provided field values, after adding the given purchase - order line in the purchase order. - - :params procurement: the procurement object generating the purchase order - :params dict po_vals: field values for the new purchase order (the - ``order_line`` field will be overwritten with one - single line, as passed in ``line_vals``). - :params dict line_vals: field values of the single purchase order line that - the purchase order will contain. - :return: id of the newly created purchase order - :rtype: int - """ - po_vals.update({'order_line': [(0,0,line_vals)]}) - return self.pool.get('purchase.order').create(cr, uid, po_vals, context=context) - - def _get_purchase_schedule_date(self, cr, uid, procurement, company, context=None): + @api.v7 + def _get_purchase_schedule_date(self, procurement): """Return the datetime value to use as Schedule Date (``date_planned``) for the Purchase Order Lines created to satisfy the given procurement. :param browse_record procurement: the procurement for which a PO will be created. - :param browse_report company: the company to which the new PO will belong to. :rtype: datetime :return: the desired Schedule Date for the PO lines """ procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT) - schedule_date = (procurement_date_planned - relativedelta(days=company.po_lead)) + schedule_date = (procurement_date_planned - relativedelta(days=procurement.company_id.po_lead)) return schedule_date + @api.v8 + def _get_purchase_order_date(self, schedule_date): + self.ensure_one() + seller_delay = int(self.product_id.seller_delay) + return schedule_date - relativedelta(days=seller_delay) + + @api.v7 def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None): """Return the datetime value to use as Order Date (``date_order``) for the Purchase Order created to satisfy the given procurement. @@ -1377,493 +644,179 @@ class procurement_order(osv.osv): seller_delay = int(procurement.product_id.seller_delay) return schedule_date - relativedelta(days=seller_delay) - def _get_product_supplier(self, cr, uid, procurement, context=None): - ''' returns the main vendor of the procurement's product given as argument''' - supplierinfo = self.pool['product.supplierinfo'] - company_supplier = supplierinfo.search(cr, uid, - [('product_tmpl_id', '=', procurement.product_id.product_tmpl_id.id), ('company_id', '=', procurement.company_id.id)], limit=1, context=context) - if company_supplier: - return supplierinfo.browse(cr, uid, company_supplier[0], context=context).name - return procurement.product_id.seller_id + @api.multi + def _prepare_purchase_order_line(self, po, supplier): + self.ensure_one() - def _get_po_line_values_from_procs(self, cr, uid, procurements, partner, schedule_date, context=None): - res = {} - if context is None: - context = {} - prod_obj = self.pool.get('product.product') - acc_pos_obj = self.pool.get('account.fiscal.position') - #Passing partner_id to context for purchase order line integrity of Line name - new_context = context.copy() - new_context.update({'lang': partner.lang, 'partner_id': partner.id}) - names = prod_obj.name_get(cr, uid, [x.product_id.id for x in procurements], context=context) - names_dict = {} - for id, name in names: - names_dict[id] = name - for procurement in procurements: - taxes_ids = procurement.product_id.supplier_taxes_id - # It is necessary to have the appropriate fiscal position to get the right tax mapping - fp = acc_pos_obj.get_fiscal_position(cr, uid, None, partner.id, context=context) - if fp: - fp = acc_pos_obj.browse(cr, uid, fp, context=context) - taxes = acc_pos_obj.map_tax(cr, uid, fp, taxes_ids) - name = names_dict[procurement.product_id.id] - if procurement.product_id.description_purchase: - name += '\n' + procurement.product_id.description_purchase - date = schedule_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - values = { - 'name': name, - 'product_id': procurement.product_id.id, - 'product_uom': procurement.product_id.uom_po_id.id, - 'date_planned': date, - 'taxes_id': [(6, 0, taxes)], - 'procurement_ids': [(4, procurement.id)] - } - # calculate price/qty for valid vendor - qty = self.pool['product.uom']._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, procurement.product_id.uom_po_id.id) - new_context.update({'quantity': qty}) - supplier = prod_obj.browse(cr, uid, procurement.product_id.id, context=new_context).selected_seller_id - price = 0.0 - if supplier: - price = supplier.price - purchase_currency = supplier.name.property_purchase_currency_id - price = self.pool['res.currency'].compute(cr, uid, supplier.currency_id.id, purchase_currency.id, price, context=dict(context or {}, date=date)) - values.update({'product_qty': qty, 'price_unit': price}) - res[procurement.id] = values - return res + res = self.env['purchase.order.line']._get_name_price_quantity_date( + self.product_id, + supplier.name, + datetime.strptime(po.date_order, DEFAULT_SERVER_DATETIME_FORMAT), + self.product_qty, + self.product_uom, + po.currency_id, + order_id=po) - def _calc_new_qty_price(self, cr, uid, procurement, po_line=None, cancel=False, context=None): - if not po_line: - po_line = procurement.purchase_line_id - uom_obj = self.pool.get('product.uom') - qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, - procurement.product_id.uom_po_id.id) - if cancel: - qty = -qty + name = res['name'] + product_qty = res['quantity'] + price_unit = res['price_unit'] + date_planned = res['date_planned'] - # Make sure we use the minimum quantity of the partner corresponding to the PO - # This does not apply in case of dropshipping - supplierinfo_min_qty = 0.0 - if po_line.order_id.location_id.usage != 'customer': - if po_line.product_id.seller_id.id == po_line.order_id.partner_id.id: - supplierinfo_min_qty = po_line.product_id.seller_qty + taxes = self.product_id.supplier_taxes_id + fpos = po.fiscal_position_id + taxes_id = fpos.map_tax(taxes) if fpos else [] + + return { + 'name': name, + 'product_qty': product_qty, + 'product_id': self.product_id.id, + 'product_uom': self.product_uom.id, + 'price_unit': price_unit, + 'date_planned': date_planned, + 'taxes_id': [(6, 0, taxes_id)], + 'procurement_ids': [(4, self.id)], + 'order_id': po.id, + } + + @api.multi + def _prepare_purchase_order(self, partner): + self.ensure_one() + schedule_date = self._get_purchase_schedule_date() + purchase_date = self._get_purchase_order_date(schedule_date) + fpos = self.env['account.fiscal.position'].get_fiscal_position(partner.id) + + gpo = self.rule_id.group_propagation_option + group = (gpo == 'fixed' and self.rule_id.group_id.id) or \ + (gpo == 'propagate' and self.group_id.id) or False + + return { + 'partner_id': partner.id, + 'picking_type_id': self.rule_id.picking_type_id.id, + 'company_id': self.company_id.id, + 'dest_address_id': self.partner_dest_id.id, + 'origin': self.origin, + 'payment_term_id': partner.property_supplier_payment_term_id.id, + 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'fiscal_position_id': fpos, + 'group_id': group + } + + @api.multi + def make_po(self): + cache = {} + res = [] + for procurement in self: + if not procurement.product_id.seller_ids: + self.message_post([procurement.id],\ + _('No vendor associated to product %s. Please set one to fix this procurement.') % (procurement.product_id.name)) + continue + supplier = procurement.product_id.seller_ids[0] + partner = supplier.name + domain = ( + ('partner_id', '=', partner.id), + ('state', '=', 'draft'), + ('picking_type_id', '=', procurement.rule_id.picking_type_id.id), + ('company_id', '=', procurement.company_id.id), + ('dest_address_id', '=', procurement.partner_dest_id.id)) + + if domain in cache: + po = cache[domain] else: - supplierinfo_obj = self.pool.get('product.supplierinfo') - supplierinfo_ids = supplierinfo_obj.search(cr, uid, [('name', '=', po_line.order_id.partner_id.id), ('product_tmpl_id', '=', po_line.product_id.product_tmpl_id.id)]) - if supplierinfo_ids: - supplierinfo = supplierinfo_obj.browse(cr, uid, supplierinfo_ids) - supplierinfo_min_qty = sorted(supplierinfo, key=lambda x: x['min_qty'])[0].min_qty - if supplierinfo_min_qty == 0.0: - qty += po_line.product_qty - else: - # Recompute quantity by adding existing running procurements. - for proc in po_line.procurement_ids: - qty += uom_obj._compute_qty(cr, uid, proc.product_uom.id, proc.product_qty, - proc.product_id.uom_po_id.id) if proc.state == 'running' else 0.0 - qty = max(qty, supplierinfo_min_qty) if qty > 0.0 else 0.0 + po = self.env['purchase.order'].search([dom for dom in domain]) + po = po[0] if po else False + cache[domain] = po + if not po: + vals = procurement._prepare_purchase_order(partner) + po = self.env['purchase.order'].create(vals) + cache[domain] = po + res += po.ids - price = po_line.price_unit - if qty != po_line.product_qty: - price = po_line.product_id.seller_price - price = self.pool['product.uom']._compute_price(cr, uid, procurement.product_uom.id, price, procurement.product_uom.id) - return qty, price + # Create Line + po_line = False + for line in po.order_line: + if line.product_id == procurement.product_id and line.product_uom == procurement.product_uom: + res = self.env['purchase.order.line']._get_name_price_quantity_date( + self.product_id, + partner, + datetime.strptime(po.date_order, DEFAULT_SERVER_DATETIME_FORMAT), + line.product_qty + procurement.product_qty, + self.product_uom, + po.currency_id) - def update_origin_po(self, cr, uid, po, proc, context=None): - pass - - def _get_grouping_dicts(self, cr, uid, ids, context=None): - """ - It will group the procurements according to the pos they should go into. That way, lines going to the same - po, can be processed at once. - Returns two dictionaries: - add_purchase_dicts: key: po value: procs to add to the po - create_purchase_dicts: key: values for proc to create (not that necessary as they are in procurement => TODO), - values: procs to add - """ - po_obj = self.pool.get('purchase.order') - date = time.strftime(DEFAULT_SERVER_DATE_FORMAT) - # Regroup POs - cr.execute(""" - SELECT psi.name, p.id, pr.id, pr.picking_type_id, p.location_id, p.partner_dest_id, p.company_id, p.group_id, - pr.group_propagation_option, pr.group_id,psi.qty - FROM procurement_order AS p - LEFT JOIN procurement_rule AS pr ON pr.id = p.rule_id - LEFT JOIN procurement_group AS pg ON p.group_id = pg.id, - product_supplierinfo AS psi, product_product AS pp - WHERE - p.product_id = pp.id AND p.id in %s AND psi.product_tmpl_id = pp.product_tmpl_id - AND (psi.company_id = p.company_id or psi.company_id IS NULL) AND ((psi.date_start IS NULL OR psi.date_start<=%s) AND (psi.date_end IS NULL OR psi.date_end>=%s)) - ORDER BY psi.sequence, - psi.name, p.rule_id, p.location_id, p.company_id, p.partner_dest_id, p.group_id - """, (tuple(ids), date, date)) - res = cr.fetchall() - old = False - # A giant dict for grouping lines, ... to do at once - create_purchase_procs = {} # Lines to add to a newly to create po - add_purchase_procs = {} # Lines to add/adjust in an existing po - proc_seller = {} # To check we only process one po - for partner, proc, rule, pick_type, location, partner_dest, company, group, group_propagation, fixed_group, qty in res: - if not proc_seller.get(proc): - proc_seller[proc] = partner - new = partner, rule, pick_type, location, company, group, group_propagation, fixed_group - if new != old: - old = new - dom = [ - ('partner_id', '=', partner), ('state', '=', 'draft'), ('picking_type_id', '=', pick_type), - ('location_id', '=', location), ('company_id', '=', company), ('dest_address_id', '=', partner_dest)] - if group_propagation == 'propagate': - dom += [('group_id', '=', group)] - elif group_propagation == 'fixed': - dom += [('group_id', '=', fixed_group)] - available_draft_po_ids = po_obj.search(cr, uid, dom, context=context) - available_draft_po = available_draft_po_ids and available_draft_po_ids[0] or False - # Add to dictionary - if available_draft_po: - if add_purchase_procs.get(available_draft_po): - add_purchase_procs[available_draft_po] += [proc] - else: - add_purchase_procs[available_draft_po] = [proc] - else: - if create_purchase_procs.get(new): - create_purchase_procs[new] += [proc] - else: - create_purchase_procs[new] = [proc] - return add_purchase_procs, create_purchase_procs - - def make_po(self, cr, uid, ids, context=None): - res = {} - po_obj = self.pool.get('purchase.order') - po_line_obj = self.pool.get('purchase.order.line') - seq_obj = self.pool.get('ir.sequence') - uom_obj = self.pool.get('product.uom') - acc_pos_obj = self.pool.get('account.fiscal.position') - add_purchase_procs, create_purchase_procs = self._get_grouping_dicts(cr, uid, ids, context=context) - procs_done = [] - - # Let us check existing purchase orders and add/adjust lines on them - for add_purchase in add_purchase_procs.keys(): - procs_done += add_purchase_procs[add_purchase] - po = po_obj.browse(cr, uid, add_purchase, context=context) - lines_to_update = {} - line_values = [] - procurements = self.browse(cr, uid, add_purchase_procs[add_purchase], context=context) - po_line_ids = po_line_obj.search(cr, uid, [('order_id', '=', add_purchase), ('product_id', 'in', [x.product_id.id for x in procurements])], context=context) - po_lines = po_line_obj.browse(cr, uid, po_line_ids, context=context) - po_prod_dict = {} - for pol in po_lines: - po_prod_dict[pol.product_id.id] = pol - procs_to_create = [] - #Check which procurements need a new line and which need to be added to an existing one - for proc in procurements: - if po_prod_dict.get(proc.product_id.id): - po_line = po_prod_dict[proc.product_id.id] - # FIXME: compute quantity using `_calc_new_qty_price` method. - # new_qty, new_price = self._calc_new_qty_price(cr, uid, proc, po_line=po_line, context=context) - uom_id = po_line.product_uom # Convert to UoM of existing line - qty = uom_obj._compute_qty_obj(cr, uid, proc.product_uom, proc.product_qty, uom_id) - - if lines_to_update.get(po_line): - lines_to_update[po_line] += [(proc, qty)] - else: - lines_to_update[po_line] = [(proc, qty)] - else: - procs_to_create.append(proc) - - # FIXME: these are not real tracking values, it should be fixed if tracking values for one2many - # are managed - def format_message(message_description, tracked_values): - message = '' - if message_description: - message = '%s' % message_description - for name, values in tracked_values.iteritems(): - message += '
    • %s: ' % name - message += '%s
' % values - return message - - # Update the quantities of the lines that need to - for line in lines_to_update.keys(): - tot_qty = 0 - for proc, qty in lines_to_update[line]: - tot_qty += qty - self.message_post(cr, uid, proc.id, body=_("Quantity added in existing Purchase Order Line"), context=context) - msg = format_message(_('Quantity added in existing Purchase Order Line'), {'Product': proc.product_id.name, 'Quantity': proc.product_qty, 'Procurement': proc.origin}) - po_obj.message_post(cr, uid, [add_purchase], body=msg, context=context) - line_values += [(1, line.id, {'product_qty': line.product_qty + tot_qty, 'procurement_ids': [(4, x[0].id) for x in lines_to_update[line]]})] - - # Create lines for which no line exists yet - if procs_to_create: - partner = po.partner_id - schedule_date = datetime.strptime(po.minimum_planned_date, DEFAULT_SERVER_DATETIME_FORMAT) - value_lines = self._get_po_line_values_from_procs(cr, uid, procs_to_create, partner, schedule_date, context=context) - line_values += [(0, 0, value_lines[x]) for x in value_lines.keys()] - for proc in procs_to_create: - self.message_post(cr, uid, [proc.id], body=_("Purchase line created and linked to an existing Purchase Order"), context=context) - msg = format_message(_('Purchase order line added'), {'Product': proc.product_id.name, 'Quantity': proc.product_qty, 'Procurement': proc.origin}) - po_obj.message_post(cr, uid, [add_purchase], body=msg, context=context) - po_obj.write(cr, uid, [add_purchase], {'order_line': line_values},context=context) - - for procurement in procurements: - self.update_origin_po(cr, uid, po, procurement, context=context) - - - # Create new purchase orders - partner_obj = self.pool.get("res.partner") - new_pos = [] - for create_purchase in create_purchase_procs.keys(): - procs_done += create_purchase_procs[create_purchase] - line_values = [] - procurements = self.browse(cr, uid, create_purchase_procs[create_purchase], context=context) - partner = partner_obj.browse(cr, uid, create_purchase[0], context=context) - - #Create purchase order itself: - procurement = procurements[0] - schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, procurement.company_id, context=context) - purchase_date = self._get_purchase_order_date(cr, uid, procurement, procurement.company_id, schedule_date, context=context) - - value_lines = self._get_po_line_values_from_procs(cr, uid, procurements, partner, schedule_date, context=context) - line_values += [(0, 0, value_lines[x]) for x in value_lines.keys()] - name = seq_obj.next_by_code(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name - gpo = procurement.rule_id.group_propagation_option - group = (gpo == 'fixed' and procurement.rule_id.group_id.id) or (gpo == 'propagate' and procurement.group_id.id) or False - fp = acc_pos_obj.get_fiscal_position(cr, uid, None, partner.id, context=context) - po_vals = { - 'name': name, - 'origin': procurement.origin, - 'partner_id': create_purchase[0], - 'location_id': procurement.location_id.id, - 'picking_type_id': procurement.rule_id.picking_type_id.id, - 'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT), - 'company_id': procurement.company_id.id, - 'currency_id': partner.property_purchase_currency_id.id or self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id.id, - 'fiscal_position_id': fp, - 'payment_term_id': partner.property_supplier_payment_term_id.id, - 'dest_address_id': procurement.partner_dest_id.id, - 'group_id': group, - 'order_line': line_values, - } - new_po = po_obj.create(cr, uid, po_vals, context=context) - new_pos.append(new_po) - for proc in create_purchase_procs[create_purchase]: - self.message_post(cr, uid, proc, body=_("Draft Purchase Order created"), context=context) - - other_proc_ids = list(set(ids) - set(procs_done)) - res = dict.fromkeys(ids, True) - if other_proc_ids: - other_procs = self.browse(cr, uid, other_proc_ids, context=context) - for procurement in other_procs: - res[procurement.id] = False - self.message_post(cr, uid, [procurement.id], _('There is no vendor associated to product %s') % (procurement.product_id.name)) + po_line = line.write({ + 'product_qty': res['quantity'], + 'price_unit': res['price_unit'], + 'procurement_ids': [(4, procurement.id)] + }) + if not po_line: + vals = procurement._prepare_purchase_order_line(po, supplier) + self.env['purchase.order.line'].create(vals) return res -class product_template(osv.Model): +class ProductTemplate(models.Model): _name = 'product.template' _inherit = 'product.template' - - def _get_buy_route(self, cr, uid, context=None): - - buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy') + + @api.model + def _get_buy_route(self): + buy_route = self.env.ref('purchase.route_warehouse0_buy') if buy_route: - return [buy_route] + return buy_route.ids return [] - def _purchase_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.purchase_count for p in template.product_variant_ids]) - return res + @api.multi + def _purchase_count(self): + for template in self: + template.purchase_count = sum([p.purchase_count for p in template.product_variant_ids]) + return True - _columns = { - 'property_account_creditor_price_difference': fields.property( - type='many2one', - relation='account.account', - string="Price Difference Account", - help="This account will be used to value price difference between purchase price and cost price."), - 'purchase_ok': fields.boolean('Can be Purchased', help="Specify if the product can be selected in a purchase order line."), - 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'), - } + property_account_creditor_price_difference = fields.Many2one('account.account', string="Price Difference Account",\ + help="This account will be used to value price difference between purchase price and cost price.") + purchase_ok = fields.Boolean('Can be Purchased', default=True) + purchase_count = fields.Integer(compute='_purchase_count', string='# Purchases') + purchase_method = fields.Selection([ + ('purchase', 'On purchased quantities'), + ('receive', 'On received quantities'), + ], string="Control Purchase Bills", default="receive") + route_ids = fields.Many2many(default=lambda self: self._get_buy_route()) - _defaults = { - 'purchase_ok': 1, - 'route_ids': _get_buy_route, - } - def action_view_purchases(self, cr, uid, ids, context=None): - products = self._get_products(cr, uid, ids, context=context) - result = self._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context) - result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]" - return result - -class product_product(osv.Model): +class ProductProduct(models.Model): _name = 'product.product' _inherit = 'product.product' - - def _purchase_count(self, cr, uid, ids, field_name, arg, context=None): - r = dict.fromkeys(ids, 0) + + @api.multi + def _purchase_count(self): domain = [ - ('state', 'in', ['confirmed', 'approved', 'except_picking', 'except_invoice', 'done']), - ('product_id', 'in', ids), + ('state', 'in', ['confirmed', 'approved', 'done']), + ('product_id', 'in', self.mapped('id')), ] - for group in self.pool['purchase.report'].read_group(cr, uid, domain, ['product_id', 'quantity'], ['product_id'], context=context): + r = {} + for group in self.env['purchase.report'].read_group(domain, ['product_id', 'quantity'], ['product_id']): r[group['product_id'][0]] = group['quantity'] - return r + for product in self: + product.purchase_count = r.get(product.id, 0) + return True - def action_view_purchases(self, cr, uid, ids, context=None): - if isinstance(ids, (int, long)): - ids = [ids] - result = self.pool['product.template']._get_act_window_dict(cr, uid, 'purchase.action_purchase_line_product_tree', context=context) - result['domain'] = "[('product_id','in',[" + ','.join(map(str, ids)) + "])]" - return result + purchase_count = fields.Integer(compute='_purchase_count', string='# Purchases') - _columns = { - 'purchase_count': fields.function(_purchase_count, string='# Purchases', type='integer'), - } - -class product_category(osv.Model): +class ProductCategory(models.Model): _inherit = "product.category" - _columns = { - 'property_account_creditor_price_difference_categ': fields.property( - type='many2one', - relation='account.account', - string="Price Difference Account", - help="This account will be used to value price difference between purchase price and cost price."), - } + + property_account_creditor_price_difference_categ = fields.Many2one( + 'account.account', string="Price Difference Account", + company_dependent=True, + help="This account will be used to value price difference between purchase price and accounting cost.") -class mail_compose_message(osv.Model): +class MailComposeMessage(models.Model): _inherit = 'mail.compose.message' - def send_mail(self, cr, uid, ids, auto_commit=False, context=None): - context = context or {} - if context.get('default_model') == 'purchase.order' and context.get('default_res_id'): - context = dict(context, mail_post_autofollow=True) - self.pool.get('purchase.order').signal_workflow(cr, uid, [context['default_res_id']], 'send_rfq') - return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context) - - -class account_invoice(osv.Model): - """ Override account_invoice to add Chatter messages on the related purchase - orders, logging the invoice receipt or payment. """ - _inherit = 'account.invoice' - - _columns = { - 'purchase_ids': fields.many2many('purchase.order', 'purchase_invoice_rel', 'invoice_id', - 'purchase_id', 'Purchases', copy=False, - help="Purchases linked to this invoice") - } - - def invoice_validate(self, cr, uid, ids, context=None): - res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context) - purchase_order_obj = self.pool.get('purchase.order') - # read access on purchase.order object is not required - if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False): - user_id = SUPERUSER_ID - else: - user_id = uid - po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context) - for po_id in po_ids: - purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice received"), context=context) - purchase_order_obj._set_po_lines_invoiced(cr, user_id, [po_id], context=context) - return res - - def confirm_paid(self, cr, uid, ids, context=None): - res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context) - purchase_order_obj = self.pool.get('purchase.order') - # read access on purchase.order object is not required - if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False): - user_id = SUPERUSER_ID - else: - user_id = uid - po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context) - for po_id in po_ids: - purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context) - return res - -class account_invoice_line(osv.Model): - """ Override account_invoice_line to add the link to the purchase order line it is related to""" - _inherit = 'account.invoice.line' - _columns = { - 'purchase_line_id': fields.many2one('purchase.order.line', - 'Purchase Order Line', ondelete='set null', select=True, - readonly=True), - } - - def move_line_get(self, cr, uid, invoice_id, context=None): - res = super(account_invoice_line,self).move_line_get(cr, uid, invoice_id, context=context) - if self.company_id.anglo_saxon_accounting: - # TDE FIXME: undefined variable - if inv.type in ('in_invoice','in_refund'): - for i_line in inv.invoice_line_ids: - res.extend(self._anglo_saxon_purchase_move_lines(cr, uid, i_line, res, context=context)) - return res - - def _anglo_saxon_purchase_move_lines(self, cr, uid, i_line, res, context=None): - """Return the additional move lines for purchase invoices and refunds. - - i_line: An account.invoice.line object. - res: The move line entries produced so far by the parent move_line_get. - """ - inv = i_line.invoice_id - company_currency = inv.company_id.currency_id.id - if i_line.product_id and i_line.product_id.valuation == 'real_time': - if i_line.product_id.type == 'product': - # get the price difference account at the product - acc = i_line.product_id.property_account_creditor_price_difference and i_line.product_id.property_account_creditor_price_difference.id - if not acc: - # if not found on the product get the price difference account at the category - acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ and i_line.product_id.categ_id.property_account_creditor_price_difference_categ.id - a = None - - # oa will be the stock input account - # first check the product, if empty check the category - oa = i_line.product_id.property_stock_account_input and i_line.product_id.property_stock_account_input.id - if not oa: - oa = i_line.product_id.categ_id.property_stock_account_input_categ_id and i_line.product_id.categ_id.property_stock_account_input_categ_id.id - if oa: - # get the fiscal position - fpos = i_line.invoice_id.fiscal_position_id or False - a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa) - diff_res = [] - account_prec = inv.company_id.currency_id.decimal_places - # calculate and write down the possible price difference between invoice price and product price - for line in res: - if line.get('invl_id', 0) == i_line.id and a == line['account_id']: - uom = i_line.product_id.uom_id - valuation_price_unit = self.pool.get('product.uom')._compute_price(cr, uid, uom.id, i_line.product_id.standard_price, i_line.uom_id.id) - if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id: - #for average/fifo/lifo costing method, fetch real cost price from incomming moves - stock_move_obj = self.pool.get('stock.move') - valuation_stock_move = stock_move_obj.search(cr, uid, [('purchase_line_id', '=', i_line.purchase_line_id.id)], limit=1, context=context) - if valuation_stock_move: - valuation_price_unit = stock_move_obj.browse(cr, uid, valuation_stock_move[0], context=context).price_unit - if inv.currency_id.id != company_currency: - valuation_price_unit = self.pool.get('res.currency').compute(cr, uid, company_currency, inv.currency_id.id, valuation_price_unit, context={'date': inv.date_invoice}) - if valuation_price_unit != i_line.price_unit and line['price_unit'] == i_line.price_unit and acc: - # price with discount and without tax included - price_unit = self.pool['account.tax'].compute_all(cr, uid, line['taxes'], i_line.price_unit * (1-(i_line.discount or 0.0)/100.0), - inv.currency_id.id, line['quantity'])['total_excluded'] - price_line = round(valuation_price_unit * line['quantity'], account_prec) - price_diff = round(price_unit - price_line, account_prec) - line.update({'price': price_line}) - diff_res.append({ - 'type': 'src', - 'name': i_line.name[:64], - 'price_unit': round(price_diff / line['quantity'], account_prec), - 'quantity': line['quantity'], - 'price': price_diff, - 'account_id': acc, - 'product_id': line['product_id'], - 'uom_id': line['uom_id'], - 'account_analytic_id': line['account_analytic_id'], - 'taxes': line.get('taxes', []), - }) - return diff_res - return [] - - -class account_invoice_line(osv.Model): - """ Override account_invoice_line to add the link to the purchase order line it is related to""" - _inherit = 'account.invoice.line' - _columns = { - 'purchase_line_ids': fields.many2many('purchase.order.line', 'purchase_order_line_invoice_rel', 'invoice_id','order_line_id', - 'Purchase Order Lines', readonly=True, copy=False) - } + @api.multi + def send_mail(self, auto_commit=False): + if self._context.get('default_model') == 'purchase.order' and self._context.get('default_res_id'): + order = self.env['purchase.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() diff --git a/addons/purchase/purchase_order_demo.yml b/addons/purchase/purchase_order_demo.yml index 8d8625b4911..1bc6e859465 100644 --- a/addons/purchase/purchase_order_demo.yml +++ b/addons/purchase/purchase_order_demo.yml @@ -3,86 +3,135 @@ - !record {model: purchase.order, id: purchase_order_1}: partner_id: base.res_partner_1 - invoice_method: order - order_line: - - product_id: product.product_product_15 - price_unit: 79.80 - product_qty: 15.0 - - product_id: product.product_product_25 - price_unit: 2868.70 - product_qty: 5.0 - - product_id: product.product_product_27 - price_unit: 3297.20 - product_qty: 4.0 - +- + !record {model: purchase.order.line, id: purchase_order_line_1}: + order_id: purchase.purchase_order_1 + product_id: product.product_product_15 + name: RAM SR3 + price_unit: 79.80 + product_qty: 15.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') +- + !record {model: purchase.order.line, id: purchase_order_line_2}: + order_id: purchase_order_1 + product_id: product.product_product_25 + name: Laptop E5023 + price_unit: 2868.70 + product_qty: 5.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') +- + !record {model: purchase.order.line, id: purchase_order_line_3}: + order_id: purchase_order_1 + product_id: product.product_product_27 + name: Laptop Customized + price_unit: 3297.20 + product_qty: 4.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') +- + Creating second order - !record {model: purchase.order, id: purchase_order_2}: partner_id: base.res_partner_3 - invoice_method: picking order_line: - product_id: product.product_product_31 + name: Multimedia Speakers price_unit: 132.50 product_qty: 20.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_29 + name: Pen drive, SP-2 price_unit: 89 product_qty: 5 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - !record {model: purchase.order, id: purchase_order_3}: partner_id: base.res_partner_12 order_line: - product_id: product.product_product_2 + name: Support Services price_unit: 25.50 product_qty: 10.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - !record {model: purchase.order, id: purchase_order_4}: partner_id: base.res_partner_4 order_line: - product_id: product.product_product_14 + name: RAM SR2 (kit) price_unit: 85.50 product_qty: 6.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_20 + name: Motherboard I9P57 price_unit: 1690 product_qty: 5.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_6 + name: iPad Mini price_unit: 800 product_qty: 7.0 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - !record {model: purchase.order, id: purchase_order_5}: partner_id: base.res_partner_2 order_line: - product_id: product.product_product_22 + name: Processor Core i5 2.70 Ghz product_qty: 3 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_24 + name: Graphics Card product_qty: 3 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - !record {model: purchase.order, id: purchase_order_6}: partner_id: base.res_partner_1 order_line: - product_id: product.product_product_38 + name: Ink Cartridge price_unit: 58 product_qty: 9 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_39 + name: Toner Cartridge product_qty: 3 price_unit: 65 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_44 + name: GrapWorks Software product_qty: 4 price_unit: 154.5 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - !record {model: purchase.order, id: purchase_order_7}: partner_id: base.res_partner_4 order_line: - product_id: product.product_product_12 + name: Mouse, Wireless product_qty: 5 + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') - product_id: product.product_product_34 + name: Webcam product_qty: 15 -- - !python {model: purchase.order} : | - account_id = self.pool.get('account.account').search(cr, uid, [('user_type_id', '=', ref('account.data_account_type_expenses'))]) - if account_id: - self.signal_workflow(cr, uid, [ref('purchase_order_2')], 'purchase_confirm') - self.signal_workflow(cr, uid, [ref('purchase_order_6')], 'purchase_confirm') + product_uom: product.product_uom_unit + date_planned: !eval time.strftime('%Y-%m-%d') diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index 6dbd7d6ab72..ec497ca41a6 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -18,9 +18,10 @@ + + action="product.product_supplierinfo_type_action" id="menu_product_pricelist_action2_purchase" + parent="menu_purchase_config_pricelist" sequence="1" groups="base.group_no_one"/> - - + + parent="menu_purchase_control" sequence="11"/> - - - - Draft Bills - account.invoice - form - tree,form,calendar,pivot,graph - - [('type','=','in_invoice')] - {'default_type':'in_invoice', 'type':'in_invoice', 'journal_type': 'purchase', 'search_default_draft': 1} - - -

- Click to create a draft invoice. -

- Use this menu to control the invoices to be received from your - vendor. Odoo generates draft invoices from your purchase - orders or receipts, according to your settings. -

- Once you receive a vendor bill, you can match it with the - draft invoice and validate it. -

-
-
- + Products @@ -108,13 +86,6 @@ - - - @@ -124,7 +95,7 @@ purchase.order - + @@ -152,30 +123,49 @@ + + Receptions + stock.picking + form + tree,form + [('purchase_id', '=', active_id)] + + purchase.order.form purchase.order
-
+
+ + +
- + - - - - - - + - - + + + + - - - + + - - - - - - - - - - - - - - - - - + + + + + + + + +
@@ -267,8 +239,6 @@ @@ -278,19 +248,14 @@ - - - - + + - - - - - - + + + @@ -305,7 +270,7 @@
- + request.quotation.select purchase.order @@ -314,45 +279,21 @@ - - - + + - + + + - + - - purchase.order.list.select - purchase.order - - - - - - - - - - - - - - - - - - - - - - @@ -381,7 +322,7 @@
- +
@@ -395,13 +336,13 @@ purchase.order.tree purchase.order - - + + - + @@ -440,7 +381,7 @@ ir.actions.act_window purchase.order tree,kanban,form,pivot,graph,calendar - {'search_default_todo':1} + {'search_default_todo':1, 'show_purchase': True} [('state','not in',('draft','sent','bid', 'confirmed'))] @@ -456,42 +397,6 @@
- - purchase.order.line.form - purchase.order.line - -
- - - - - - - - - - - - - - - - - - - - - -
-
-
- purchase.order.line.tree purchase.order.line @@ -506,12 +411,10 @@ - - - + purchase.order.line.form2 purchase.order.line @@ -521,7 +424,7 @@ - - Purchase Order Lines - ir.actions.act_window - purchase.order.line - [('order_id.invoice_method','=','manual'), ('state','in',('confirmed','done')), ('invoiced', '=', False)] - form - tree,form - - -

- Here you can track all the lines of purchase orders where the - invoicing is "Based on Purchase Order Lines", and for which you - have not received a vendor bill yet. You can generate a - draft vendor bill based on the lines from this list. -

-
-
- - - - tree - - - - - - form - - - - - @@ -636,6 +502,9 @@ @@ -664,7 +533,8 @@
- {} + {} + [('product_id','in',active_ids)] Purchases purchase.order.line @@ -691,25 +561,9 @@
- -
-
-
- - - product.product.purchase.button.inherit - product.product - - - -
-
diff --git a/addons/purchase/purchase_workflow.xml b/addons/purchase/purchase_workflow.xml deleted file mode 100644 index 771bed2685a..00000000000 --- a/addons/purchase/purchase_workflow.xml +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - Purchase Order Basic Workflow - purchase.order - True - - - - - True - draft - - - - sent - function - write({'state':'sent'}) - - - - bid - function - wkf_bid_received() - - - - confirmed - OR - function - wkf_confirm_order() - - - - cancel - function - True - wkf_action_cancel() - - - - except_invoice - function - write({'state':'except_invoice'}) - - - - except_picking - function - write({'state':'except_picking'}) - - - - router - OR - function - AND - wkf_approve_order() - - - - invoice - subflow - - action_invoice_create() - - - - invoice_done - invoice_done() - function - - - - invoice_end - - - - picking - function - action_picking_create() - - - - - - - picking_done - picking_done() - function - - - - done - wkf_po_done() - function - True - AND - - - - - - purchase_confirm - - - - - send_rfq - - - - - purchase_confirm - - - - - bid_received - - - - - purchase_cancel - - - - - purchase_cancel - - - - - - purchase_cancel - - - - - purchase_cancel - - - - - - - - - - has_stockable_product() - - - - - not has_stockable_product() - - - - - invoice_method=='order' - - - - - invoice_method<>'order' - - - - - picking_ok - - - - - invoice_ok - - - - - purchase_cancel - - - - - purchase_cancel - - - - - picking_cancel - - - - - subflow.cancel - - - - - picking_done - - - - - - subflow.paid - - - - - - - - - - - - - invoiced - - - - - purchase_cancel - invoice_method<>'order' - - - - diff --git a/addons/purchase/report/purchase_report.py b/addons/purchase/report/purchase_report.py index d8443d88db2..e741464340a 100644 --- a/addons/purchase/report/purchase_report.py +++ b/addons/purchase/report/purchase_report.py @@ -14,20 +14,17 @@ class purchase_report(osv.osv): _auto = False _columns = { 'date': fields.datetime('Order Date', readonly=True, help="Date on which this document has been created"), # TDE FIXME master: rename into date_order - 'state': fields.selection([('draft', 'Request for Quotation'), - ('confirmed', 'Waiting Vendor Ack'), - ('approved', 'Approved'), - ('except_picking', 'Shipping Exception'), - ('except_invoice', 'Invoice Exception'), - ('done', 'Done'), - ('cancel', 'Cancelled')],'Order Status', readonly=True), + 'state': fields.selection([('draft', 'Draft RFQ'), + ('sent', 'RFQ Sent'), + ('to approve', 'To Approve'), + ('purchase', 'Purchase Order'), + ('done', 'Done'), + ('cancel', 'Cancelled') + ],'Order Status', readonly=True), 'product_id':fields.many2one('product.product', 'Product', readonly=True), 'picking_type_id': fields.many2one('stock.warehouse', 'Warehouse', readonly=True), - 'location_id': fields.many2one('stock.location', 'Destination', readonly=True), 'partner_id':fields.many2one('res.partner', 'Vendor', readonly=True), 'date_approve':fields.date('Date Approved', readonly=True), - 'expected_date':fields.date('Expected Date', readonly=True), - 'validator' : fields.many2one('res.users', 'Validated By', readonly=True), 'product_uom' : fields.many2one('product.uom', 'Reference Unit of Measure', required=True), 'company_id':fields.many2one('res.company', 'Company', readonly=True), 'currency_id': fields.many2one('res.currency', 'Currency', readonly=True), @@ -64,11 +61,9 @@ class purchase_report(osv.osv): select min(l.id) as id, s.date_order as date, - l.state, + s.state, s.date_approve, - s.minimum_planned_date as expected_date, s.dest_address_id, - s.validator, spt.warehouse_id as picking_type_id, s.partner_id as partner_id, s.create_uid as user_id, @@ -78,7 +73,6 @@ class purchase_report(osv.osv): p.product_tmpl_id, t.categ_id as category_id, t.uom_id as product_uom, - s.location_id as location_id, sum(l.product_qty/u.factor*u2.factor) as quantity, extract(epoch from age(s.date_approve,s.date_order))/(24*60*60)::decimal(16,2) as delay, extract(epoch from age(l.date_planned,s.date_order))/(24*60*60)::decimal(16,2) as delay_pass, @@ -108,20 +102,17 @@ class purchase_report(osv.osv): s.create_uid, s.partner_id, u.factor, - s.location_id, l.price_unit, s.date_approve, l.date_planned, l.product_uom, - s.minimum_planned_date, - s.validator, s.dest_address_id, s.fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id, s.date_order, - l.state, + s.state, spt.warehouse_id, u.uom_type, u.category_id, diff --git a/addons/purchase/report/purchase_report_view.xml b/addons/purchase/report/purchase_report_view.xml index 7fa9927ce2a..9d8ec2f38bd 100644 --- a/addons/purchase/report/purchase_report_view.xml +++ b/addons/purchase/report/purchase_report_view.xml @@ -61,13 +61,10 @@ - - - diff --git a/addons/purchase/res_config.py b/addons/purchase/res_config.py index 5732ec807b0..f903d762a70 100644 --- a/addons/purchase/res_config.py +++ b/addons/purchase/res_config.py @@ -15,11 +15,6 @@ class purchase_config_settings(osv.osv_memory): ], "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'), - 'default_invoice_method': fields.selection( - [('manual', 'Control vendor bill on purchase order line'), - ('picking', 'Control vendor bill on incoming shipments'), - ('order', 'Control vendor bill on a pregenerated draft invoice'), - ], 'Default invoicing control method', required=True, default_model='purchase.order'), 'group_uom':fields.selection([ (0, 'Products have only one unit of measure (easier)'), (1, 'Some products may be sold/puchased in different unit of measures (advanced)') @@ -53,10 +48,6 @@ class purchase_config_settings(osv.osv_memory): '-This installs the module stock_dropshipping.'), } - _defaults = { - 'default_invoice_method': 'order', - } - class account_config_settings(osv.osv_memory): _inherit = 'account.config.settings' diff --git a/addons/purchase/res_config_view.xml b/addons/purchase/res_config_view.xml index 3f767c0b349..6d6684d76f9 100644 --- a/addons/purchase/res_config_view.xml +++ b/addons/purchase/res_config_view.xml @@ -22,12 +22,6 @@ - - - - -
- Stock Warehouse Inherited stock.warehouse @@ -60,28 +34,5 @@
- - Incoming Shipments - stock.picking - ir.actions.act_window - form - tree,form,calendar - {"default_type": "in", "contact_display": "partner_address", "search_default_done": 1, "search_default_reception_to_invoice": 1} - - -

- Click to create a new incoming shipment. -

- Here you can track all the product receipts of purchase - orders where the invoicing is "Based on Incoming Shipments", - and for which you have not received a supplier bill yet. - You can generate a supplier bill based on those receipts. -

-
-
- - diff --git a/addons/purchase/test/anglo_saxon.yml b/addons/purchase/test/anglo_saxon.yml deleted file mode 100644 index 72e16a0d47f..00000000000 --- a/addons/purchase/test/anglo_saxon.yml +++ /dev/null @@ -1,247 +0,0 @@ -- - I first set the company to use anglo saxon accounting. -- - !python {model: res.company}: | - self.pool.get('res.company').write(cr, uid, ref("base.main_company"), {'anglo_saxon_accounting': True,}) -- - In order to test anglo_saxon Configure Different Accounts. -- - !record {model: account.account, id: account_anglo_stock_valuation}: - code: X3000 - name: Stock Valuation Account- (test) - user_type_id: account.data_account_type_current_assets -- - Configure Stock Interim account (Received). -- - !record {model: account.account, id: account_anglo_stock_input}: - code: X2800 - name: Stock Interim account (Received) - user_type_id: account.data_account_type_expenses -- - Configure Stock Interim account (Delivered). -- - !record {model: account.account, id: account_anglo_stock_output}: - code: X2801 - name: Stock Interim account (Delivered) - user_type_id: account.data_account_type_revenue -- - Configure Price difference creditor Account. -- - !record {model: account.account, id: account_anglo_price_difference}: - code: X7095 - name: Price difference creditor Account - user_type_id: account.data_account_type_expenses -- - Configure Cash Bank Account. -- - !record {model: account.account, id: account_anglo_cash}: - code: X5000 - name: Cash/Bank Account - user_type_id: account.data_account_type_liquidity - reconcile: True -- - Configure Creditor Account Payable. -- - !record {model: account.account, id: account_anglo_payable}: - code: X440001 - name: Creditor Account Payable - user_type_id: account.data_account_type_payable - reconcile: True -- - Configure Debtor Account Receivable. -- - !record {model: account.account, id: account_anglo_receivable}: - code: X400001 - name: Debtor Account Receivable - user_type_id: account.data_account_type_receivable - reconcile: True -- - Configure Cost of Good sale Account. -- - !record {model: account.account, id: account_anglo_cogs}: - code: X7000 - name: Cost of goods sale account - user_type_id: account.data_account_type_expenses -- - Configure Income Account. -- - !record {model: account.account, id: account_anglo_income}: - code: X8000 - name: Income Account - user_type_id: account.data_account_type_revenue -- - I configure the account receivable of supplier -- - !record {model: res.partner, id: base.res_partner_3}: - property_account_payable_id: account_anglo_payable - property_account_receivable_id: account_anglo_receivable -- - I configure the account receivable of Customer. -- - !record {model: res.partner, id: base.res_partner_2}: - property_account_payable_id: account_anglo_payable - property_account_receivable_id: account_anglo_receivable -- - I configure the product category with stock valuation account. -- - !record {model: product.category, id: product.product_category_5}: - property_stock_valuation_account_id: account_anglo_stock_valuation -- - I configure the product with required accounts, and cost method = standard -- - !python {model: product.product}: | - self.write(cr, uid, [ref('product.product_product_10')], {'list_price': 20.00,'standard_price': 9,'categ_id': ref('product.product_category_5'),'valuation': 'real_time', - 'property_account_income_id': ref('account_anglo_income'),'property_account_expense_id': ref('account_anglo_cogs'), - 'property_account_creditor_price_difference': ref('account_anglo_price_difference'),'property_stock_account_input': ref('account_anglo_stock_input'), - 'property_stock_account_output': ref('account_anglo_stock_output'), 'cost_method': 'standard'}) -- - I create a draft Purchase Order. -- - !record {model: purchase.order, id: purchase_order_001}: - partner_id: base.res_partner_3 - location_id: stock.stock_location_stock - order_line: - - product_id: product.product_product_10 - product_qty: 1 - price_unit: 10 - date_planned: !eval "'%s' % (time.strftime('%Y-%m-%d'))" -- - I confirm the purchase order. -- - !workflow {model: purchase.order, ref: purchase_order_001, action: purchase_confirm} -- - Receipt is ready for process so now we do it. -- - !python {model: stock.picking}: | - picking_id = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_001"), context=context).picking_ids[0] - picking_id.do_transfer() -- - I check the Stock Interim account (Received) is credited successfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_stock_input')).credit - # float_compare(credit, 9, precision_digits=2) == 0, "Stock Interim account (Received) is not credited successfully." -- - I check the Stock valuation account is debited sucessfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_stock_valuation')).debit - # float_compare(debit, 9, precision_digits=2) == 0, "Stock valuation account is not debited successfully." -- - I Validate Invoice of Purchase Order. -- - !python {model: purchase.order}: | - invoice_ids = [x.id for x in self.browse(cr, uid, ref("purchase_order_001")).invoice_ids] - self.pool.get('account.invoice').signal_workflow(cr, uid, invoice_ids, 'invoice_open') -- - I check the Stock Interim account (Received) is debited sucessfully when Invoice validated. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_stock_input')).debit - # float_compare(debit, 9, precision_digits=2) == 0, "Stock Interim account (Received) is not debited successfully." -- - I check the Price difference creditor Account is debited sucessfully when Invoice validated. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_price_difference')).debit - # float_compare(debit, 1, precision_digits=2) == 0, "Price difference creditor Account is not debited successfully." -- - I check Payable(creditor) Account is Credited sucessfully when Invoice validated. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_payable')).credit - # float_compare(credit, 10, precision_digits=2) == 0, "Payable(creditor) Account is not Credited successfully." -- - I open the Invoice. -- - !python {model: purchase.order}: | - po = self.browse(cr, uid, ref("purchase_order_001")) - for invoice in po.invoice_ids: - invoice.signal_workflow('invoice_open') -- - I pay the invoice. -- - !python {model: purchase.order}: | - invoice_ids = self.browse(cr, uid, ref("purchase_order_001")).invoice_ids - order = self.browse(cr, uid, ref("purchase_order_001")) - 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 invoice_ids: - self.pool.get('account.invoice').pay_and_reconcile(cr, uid, [invoice.id], journal_ids[0]) -- - I check Payable(Creditors) Account is Debited sucessfully after invoice paid. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_payable')).debit - # assert float_compare(debit, 10, precision_digits=2) == 0, "Payable(Creditors) Account is not Debited successfully." -- - I check Bank/Cash account is credited sucessfully after invoice paid. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_cash')).credit - # float_compare(credit, 10, precision_digits=2) == 0, "Bank/Cash account is not credited successfully." -- - I create an Outgoing Picking order -- - !record {model: stock.picking, id: stock_picking_out001}: - partner_id: base.res_partner_2 - move_lines: - - company_id: base.main_company - location_id: stock.stock_location_stock - product_id: product.product_product_10 - product_uom_qty: 1.0 - product_uom: product.product_uom_unit - location_dest_id: stock.stock_location_customers - move_type: direct - picking_type_id: stock.picking_type_out -- - I need to check the availability of the product, So I make my picking order for processing later. -- - !python {model: stock.picking}: | - self.action_confirm(cr, uid, [ref('stock_picking_out001')], context=context) - -- - I check the product availability, Product is available in the stock and ready to be sent. -- - !python {model: stock.picking}: | - picking = self.browse(cr, uid, ref("stock_picking_out001")) - assert picking.state == "confirmed", "Picking should be confirmed." - for move_line in picking.move_lines: - assert move_line.state == "confirmed", "Move should be confirmed." - -- - I process the delivery. -- - !python {model: stock.picking}: | - picking = self.pool.get('stock.picking').browse(cr, uid, ref("stock_picking_out001"), context=context) - picking.do_transfer() -- - I check Stock Interim account (Delivery) is debited successfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_stock_output')).debit - # float_compare(debit, 9, precision_digits=2) == 0, "Stock Interim account (Delivery) is not debited successfully." -- - I check the Stock valuation account is credited sucessfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_stock_valuation')).credit - # float_compare(credit, 9, precision_digits=2) == 0, "Stock valuation account is not credited successfully." diff --git a/addons/purchase/test/anglo_saxon_avg_fifo.yml b/addons/purchase/test/anglo_saxon_avg_fifo.yml deleted file mode 100644 index 019605e0735..00000000000 --- a/addons/purchase/test/anglo_saxon_avg_fifo.yml +++ /dev/null @@ -1,247 +0,0 @@ -- - I first set the company to use anglo saxon accounting. -- - !python {model: res.company}: | - self.pool.get('res.company').write(cr, uid, ref("base.main_company"), {'anglo_saxon_accounting': True,}) -- - In order to test anglo_saxon Configure Different Accounts. -- - !record {model: account.account, id: account_anglo_stock_valuation_fifo}: - code: X3000f - name: Stock Valuation Account- (test) - user_type_id: account.data_account_type_current_assets -- - Configure Stock Interim account (Received). -- - !record {model: account.account, id: account_anglo_stock_input_fifo}: - code: X2800f - name: Stock Interim account (Received) - user_type_id: account.data_account_type_expenses -- - Configure Stock Interim account (Delivered). -- - !record {model: account.account, id: account_anglo_stock_output_fifo}: - code: X2801f - name: Stock Interim account (Delivered) - user_type_id: account.data_account_type_revenue -- - Configure Price difference creditor Account. -- - !record {model: account.account, id: account_anglo_price_difference_fifo}: - code: X7095f - name: Price difference creditor Account - user_type_id: account.data_account_type_expenses -- - Configure Cash Bank Account. -- - !record {model: account.account, id: account_anglo_cash_fifo}: - code: X5000f - name: Cash/Bank Account - user_type_id: account.data_account_type_liquidity - reconcile: True -- - Configure Creditor Account Payable. -- - !record {model: account.account, id: account_anglo_payable_fifo}: - code: X440001f - name: Creditor Account Payable - user_type_id: account.data_account_type_payable - reconcile: True -- - Configure Debtor Account Receivable. -- - !record {model: account.account, id: account_anglo_receivable_fifo}: - code: X400001f - name: Debtor Account Receivable - user_type_id: account.data_account_type_receivable - reconcile: True -- - Configure Cost of Good sale Account. -- - !record {model: account.account, id: account_anglo_cogs_fifo}: - code: X7000f - name: Cost of goods sale account - user_type_id: account.data_account_type_expenses -- - Configure Income Account. -- - !record {model: account.account, id: account_anglo_income_fifo}: - code: X8000f - name: Income Account - user_type_id: account.data_account_type_revenue -- - I configure the account receivable of supplier -- - !record {model: res.partner, id: base.res_partner_3}: - property_account_payable_id: account_anglo_payable_fifo - property_account_receivable_id: account_anglo_receivable_fifo -- - I configure the account receivable of Customer. -- - !record {model: res.partner, id: base.res_partner_2}: - property_account_payable_id: account_anglo_payable_fifo - property_account_receivable_id: account_anglo_receivable_fifo -- - I configure the product category with stock valuation account. -- - !record {model: product.category, id: product.product_category_5}: - property_stock_valuation_account_id: account_anglo_stock_valuation_fifo -- - I create a product with required accounts, and cost method average (but same applies for fifo) -- - !record {model: product.product, id: product_fifo_anglo_saxon}: - name: 'FIFO product for anglo saxon tests' - lst_price: 20.00 - standard_price: 0 - categ_id: product.product_category_5 - valuation: 'real_time' - property_account_income_id: account_anglo_income_fifo - property_account_expense_id: account_anglo_cogs_fifo - property_account_creditor_price_difference: account_anglo_price_difference_fifo - property_stock_account_input: account_anglo_stock_input_fifo - property_stock_account_output: account_anglo_stock_output_fifo - cost_method: 'average' -- - I create a draft Purchase Order. -- - !record {model: purchase.order, id: purchase_order_001_fifo}: - partner_id: base.res_partner_3 - location_id: stock.stock_location_stock - order_line: - - product_id: product_fifo_anglo_saxon - product_qty: 1 - price_unit: 9 - date_planned: !eval "'%s' % (time.strftime('%Y-%m-%d'))" - taxes_id: [] -- - I confirm the purchase order. -- - !workflow {model: purchase.order, ref: purchase_order_001_fifo, action: purchase_confirm} -- - Receipt is ready for process so now we do it. -- - !python {model: stock.picking}: | - picking_id = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_001_fifo"), context=context).picking_ids[0] - picking_id.do_transfer() -- - I check the Stock Interim account (Received) is credit successfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_stock_input_fifo')).credit - # assert float_compare(credit, 9, precision_digits=2) == 0, "Stock Interim account (Received) is not credited successfully." -- - I check the Stock valuation account is debit sucessfully. -- - !python {model: account.account, id}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_stock_valuation_fifo')).debit - # assert float_compare(debit, 9, precision_digits=2) == 0, "Stock valuation account is not debited successfully." -- - I Validate Invoice of Purchase Order after having changed the price to 10. -- - !python {model: purchase.order}: | - invoice_ids = [x.id for x in self.browse(cr, uid, ref("purchase_order_001_fifo")).invoice_ids] - line_ids = self.pool.get('account.invoice.line').search(cr, uid, [('invoice_id', 'in', invoice_ids)]) - self.pool.get('account.invoice.line').write(cr, uid, line_ids, {'price_unit': 10}) - self.pool.get('account.invoice').signal_workflow(cr, uid, invoice_ids, 'invoice_open') -- - I check the Stock Interim account (Received) is debited sucessfully when Invoice validated. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_stock_input_fifo')).debit - # assert float_compare(debit, 9, precision_digits=2) == 0, "Stock Interim account (Received) is not debited successfully." -- - I check the Price difference creditor Account is debited sucessfully when Invoice validated. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_price_difference_fifo')).debit - # assert float_compare(debit, 1, precision_digits=2) == 0, "Price difference creditor Account is not debited successfully." -- - I check Payable(creditor) Account is Credited sucessfully when Invoice validated. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_payable_fifo')).credit - # assert float_compare(credit, 10, precision_digits=2) == 0, "Payable(creditor) Account is not Credited successfully." -- - I pay the invoice. -- - !python {model: purchase.order}: | - invoice_ids = self.browse(cr, uid, ref("purchase_order_001_fifo")).invoice_ids - order = self.browse(cr, uid, ref("purchase_order_001_fifo")) - 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 invoice_ids: - self.pool.get('account.invoice').pay_and_reconcile(cr, uid, [invoice.id], journal_ids[0]) -- - I check Payable(Creditors) Account is Debited sucessfully after invoice paid. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_payable_fifo')).debit - # assert float_compare(debit, 10, precision_digits=2) == 0, "Payable(Creditors) Account is not Debited successfully." -- - I check Bank/Cash account is credited sucessfully after invoice paid. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_cash_fifo')).credit - # assert float_compare(credit, 10, precision_digits=2) == 0, "Bank/Cash account is not credited successfully." -- - I create an Outgoing Picking order -- - !record {model: stock.picking, id: stock_picking_out001_fifo}: - partner_id: base.res_partner_2 - move_lines: - - company_id: base.main_company - location_id: stock.stock_location_stock - product_id: product_fifo_anglo_saxon - product_uom_qty: 1.0 - location_dest_id: stock.stock_location_customers - move_type: direct - picking_type_id: stock.picking_type_out -- - I need to check the availability of the product, So I make my picking order for processing later. -- - !python {model: stock.picking}: | - self.action_confirm(cr, uid, [ref('stock_picking_out001_fifo')], context=context) -- - I check the product availability, Product is available in the stock and ready to be sent. -- - !python {model: stock.picking}: | - picking = self.browse(cr, uid, ref("stock_picking_out001_fifo")) - assert picking.state == "confirmed", "Picking should be confirmed." - for move_line in picking.move_lines: - assert move_line.state == "confirmed", "Move should be confirmed." -- - I process the delivery. -- - !python {model: stock.picking}: | - picking = self.pool.get('stock.picking').browse(cr, uid, ref("stock_picking_out001_fifo"), context=context) - picking.do_transfer() -- - I check Stock Interim account (Delivery) is debited successfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to debit field is removed - # debit = self.browse(cr, uid, ref('account_anglo_stock_output_fifo')).debit - # assert float_compare(debit, 9, precision_digits=2) == 0, "Stock Interim account (Delivery) is not debited successfully." -- - I check the Stock valuation account is credited sucessfully. -- - !python {model: account.account}: | - from openerp.tools import float_compare - # Due to credit field is removed - # credit = self.browse(cr, uid, ref('account_anglo_stock_valuation_fifo')).credit - # assert float_compare(credit, 9, precision_digits=2) == 0, "Stock valuation account is not credited successfully." diff --git a/addons/purchase/test/average_price.yml b/addons/purchase/test/average_price.yml index b816e34bcbc..3d34e17aeff 100644 --- a/addons/purchase/test/average_price.yml +++ b/addons/purchase/test/average_price.yml @@ -18,7 +18,6 @@ - !record {model: purchase.order, id: purchase_order_average1}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_average_icecream product_qty: 10.0 @@ -28,12 +27,13 @@ - I confirm the first purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_average1} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_average1")]) - I check the "Approved" status of purchase order 1 - !assert {model: purchase.order, id: purchase_order_average1}: - - state == 'approved' + - state == 'purchase' - Process the reception of purchase order 1 - @@ -52,7 +52,6 @@ - !record {model: purchase.order, id: purchase_order_average2}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_average_icecream product_qty: 30.0 @@ -62,7 +61,8 @@ - I confirm the second purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_average2} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_average2")]) - Process the reception of purchase order 2 - @@ -106,7 +106,6 @@ - !record {model: purchase.order, id: purchase_order_average3}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_average_icecream product_qty: 500.0 @@ -116,7 +115,8 @@ - I confirm the first purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_average3} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_average3")]) - Process the reception of purchase order 3 in grams - diff --git a/addons/purchase/test/costmethodchange.yml b/addons/purchase/test/costmethodchange.yml index dccf5ee10c5..cad69ecf3b5 100644 --- a/addons/purchase/test/costmethodchange.yml +++ b/addons/purchase/test/costmethodchange.yml @@ -19,7 +19,6 @@ - !record {model: purchase.order, id: purchase_order_variable1}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_variable_icecream product_qty: 10.0 @@ -31,7 +30,6 @@ - !record {model: purchase.order, id: purchase_order_variable2}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_variable_icecream product_qty: 30.0 @@ -41,16 +39,18 @@ - I confirm the first purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_variable1} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_variable1")]) - I confirm the second purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_variable2} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_variable2")]) - I check the "Approved" status of purchase order 1 - !assert {model: purchase.order, id: purchase_order_variable1}: - - state == 'approved' + - state == 'purchase' - Process the reception of purchase order 1 - @@ -113,7 +113,6 @@ - !record {model: purchase.order, id: purchase_order_variable3}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_variable_icecream product_qty: 20.0 @@ -123,7 +122,8 @@ - I confirm this third purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_variable3} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_variable3")]) - Process the reception of purchase order 3 - diff --git a/addons/purchase/test/fifo_price.yml b/addons/purchase/test/fifo_price.yml index 05d87991489..2458a805862 100644 --- a/addons/purchase/test/fifo_price.yml +++ b/addons/purchase/test/fifo_price.yml @@ -26,7 +26,6 @@ !record {model: purchase.order, id: purchase_order_fifo1}: partner_id: base.res_partner_3 currency_id: base.EUR - location_id: stock.stock_location_stock order_line: - product_id: product_fifo_icecream product_qty: 10.0 @@ -36,12 +35,13 @@ - I confirm the first purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo1} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fifo1")]) - - I check the "Approved" status of purchase order 1 + I check the "Purchase" status of purchase order 1 - !assert {model: purchase.order, id: purchase_order_fifo1}: - - state == 'approved' + - state == 'purchase' - Process the reception of purchase order 1 and set date - @@ -58,7 +58,6 @@ - !record {model: purchase.order, id: purchase_order_fifo2}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_fifo_icecream product_qty: 30.0 @@ -68,7 +67,8 @@ - I confirm the second purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo2} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fifo2")]) - Process the reception of purchase order 2 - @@ -160,7 +160,6 @@ !record {model: purchase.order, id: purchase_order_fifo_usd}: partner_id: base.res_partner_3 currency_id: new_USD - location_id: stock.stock_location_stock order_line: - product_id: product_fifo_icecream product_qty: 30000 @@ -174,7 +173,8 @@ - I confirm the purchase order in USD - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo_usd} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fifo_usd")]) - Process the reception of purchase order with usd - @@ -202,7 +202,7 @@ - !python {model: stock.picking}: | self.action_assign(cr, uid, [ref("outgoing_fifo_shipment_cur")]) -- +- Process the delivery of the outgoing shipment - !python {model: stock.picking}: | @@ -218,7 +218,7 @@ - !record {model: stock.picking, id: outgoing_fifo_shipment_ret}: picking_type_id: stock.picking_type_out -- +- Picking needs movement from stock - !record {model: stock.move, id: outgoing_shipment_fifo_icecream_ret}: @@ -320,7 +320,6 @@ - !record {model: purchase.order, id: purchase_order_fifo_neg}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_fifo_negative product_qty: 50.0 @@ -330,7 +329,8 @@ - I confirm the first purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo_neg} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fifo_neg")]) - Process the reception of purchase order 1 - @@ -348,7 +348,6 @@ - !record {model: purchase.order, id: purchase_order_fifo_neg2}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_fifo_negative product_qty: 60.0 @@ -358,7 +357,8 @@ - I confirm the second negative purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo_neg2} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fifo_neg2")]) - Process the reception of purchase order 2 - diff --git a/addons/purchase/test/fifo_returns.yml b/addons/purchase/test/fifo_returns.yml index 77ba3984715..11289188f53 100644 --- a/addons/purchase/test/fifo_returns.yml +++ b/addons/purchase/test/fifo_returns.yml @@ -19,7 +19,6 @@ - !record {model: purchase.order, id: purchase_order_fiforet1}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_fiforet_icecream product_qty: 10.0 @@ -31,7 +30,6 @@ - !record {model: purchase.order, id: purchase_order_fiforet2}: partner_id: base.res_partner_3 - location_id: stock.stock_location_stock order_line: - product_id: product_fiforet_icecream product_qty: 30.0 @@ -41,7 +39,8 @@ - I confirm the first purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fiforet1} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fiforet1")]) - Process the reception of purchase order 1 - @@ -56,7 +55,8 @@ - I confirm the second purchase order - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fiforet2} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_fiforet2")]) - Process the reception of purchase order 2 - diff --git a/addons/purchase/test/process/cancel_order.yml b/addons/purchase/test/process/cancel_order.yml index d3695cba6c8..6051f50c7fa 100644 --- a/addons/purchase/test/process/cancel_order.yml +++ b/addons/purchase/test/process/cancel_order.yml @@ -8,50 +8,24 @@ - I confirm the purchase order. - - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_5} + !python {model: purchase.order}: | + self.button_confirm(cr, uid, [ref("purchase_order_5")]) - I check the "Approved" status after confirmed RFQ. - !assert {model: purchase.order, id: purchase_order_5}: - - state == 'approved' + - state == 'purchase' - First I cancel receptions related to this order if order shipped. - !python {model: purchase.order}: | order = self.browse(cr, uid, ref("purchase_order_5")) self.pool.get('stock.picking').action_cancel(cr, uid, [picking.id for picking in order.picking_ids]) -- - I check order status in "Shipping Exception". -- - !python {model: purchase.order}: | - order = self.browse(cr, uid, ref("purchase_order_5")) - assert order.state == "except_picking", "order should be in Ship Exception state after cancel shipment" - Now I am able to cancel purchase order. - !python {model: purchase.order}: | - self.action_cancel(cr, uid, [ref("purchase_order_5")]) -- - I check that order is cancelled. -- - !assert {model: purchase.order, id: purchase_order_5}: - - state == 'cancel' -- - After cancel the order, I check that it's related invoice cancelled. -- - !python {model: purchase.order}: | - order = self.browse(cr, uid, ref("purchase_order_5")) - assert order.invoice_ids[0].state == "cancel", "order's related invoice should be cancelled" -- - Now again set cancelled order to draft. -- - !python {model: purchase.order}: | - self.action_cancel_draft(cr, uid, [ref("purchase_order_5")]) -- - Now I again to cancel draft order. -- - !python {model: purchase.order}: | - self.action_cancel(cr, uid, [ref("purchase_order_5")]) + self.button_cancel(cr, uid, [ref("purchase_order_5")]) - I check that order is cancelled. - diff --git a/addons/purchase/test/process/generate_invoice_from_reception.yml b/addons/purchase/test/process/generate_invoice_from_reception.yml deleted file mode 100644 index 6e3a7d3d8e0..00000000000 --- a/addons/purchase/test/process/generate_invoice_from_reception.yml +++ /dev/null @@ -1,29 +0,0 @@ -- - Purchase user can create an invoice for order on receptions therfore test with that user which have Purchase user rights. -- - !context - uid: 'res_users_purchase_user' -- - I confirm another order where invoice control is 'Bases on incoming shipments'. -- - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_2} -- - I check that the invoice of order. -- - !python {model: purchase.order}: | - purchase_order = self.browse(cr, uid, ref("purchase_order_2")) - assert len(purchase_order.invoice_ids) == 0, "Invoice should not be generated on order confirmation." -- - Now I create an invoice for order on reception. -- - !python {model: purchase.order}: | - pick_ids = [self.browse(cr, uid, ref("purchase_order_2")).id] - self.action_invoice_create(cr, uid, pick_ids) -- - I check that the invoice of order. -- - !python {model: purchase.order}: | - purchase_order = self.browse(cr, uid, ref("purchase_order_2")) - assert len(purchase_order.invoice_ids) == 1, "Invoice should be generated." - - diff --git a/addons/purchase/test/process/invoice_on_poline.yml b/addons/purchase/test/process/invoice_on_poline.yml deleted file mode 100644 index d31b371f009..00000000000 --- a/addons/purchase/test/process/invoice_on_poline.yml +++ /dev/null @@ -1,25 +0,0 @@ -- - Purchase User confirm the order and create invoice based on purchase order line. -- - !context - uid: 'res_users_purchase_user' -- - I confirm purchase order which has invoicing control method "Based on Purchase Order Lines". -- - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_6} -- - In order to test create invoice for purchase order line. -- - !python {model: purchase.order}: | - invoice_line_obj = self.pool.get('purchase.order.line_invoice') - purchase_order = self.browse(cr, uid, ref("purchase_order_6")) - context.update({'active_model': 'purchase.order', 'active_ids': [ref("purchase_order_6")]}) - for purchase_line in purchase_order.order_line: - invoice_line_obj.makeInvoices(cr, uid, purchase_line.id, context=context) -- - I check the invoice of order. -- - !python {model: purchase.order}: | - purchase_order = self.browse(cr, uid, ref("purchase_order_6")) - for purchase_line in purchase_order.order_line: - assert len(purchase_order.invoice_ids) == 1, "Invoice should be generated." \ No newline at end of file diff --git a/addons/purchase/test/process/merge_order.yml b/addons/purchase/test/process/merge_order.yml deleted file mode 100644 index 591556a6981..00000000000 --- a/addons/purchase/test/process/merge_order.yml +++ /dev/null @@ -1,44 +0,0 @@ -- - Give access rights of Purchase user to merge two RFQ. -- - !context - uid: 'res_users_purchase_user' -- - In order to merge RFQ, I merge two RFQ which has same supplier and check new merged order. -- - !python {model: purchase.order}: | - context.update({"mail_create_nolog" : True }) - order3 = self.browse(cr, uid, ref('purchase_order_4')) - order7 = self.browse(cr, uid, ref('purchase_order_7')) - total_qty = sum([x.product_qty for x in order3.order_line] + [x.product_qty for x in order7.order_line]) - new_id = self.do_merge(cr, uid, [ref('purchase_order_4'), ref('purchase_order_7')], context=context) - - assert order3.state == 'cancel', "Merged order should be canceled" - assert order7.state == 'cancel', "Merged order should be canceled" - - def merged_data(lines): - product_id =[] - product_uom = [] - res = {} - for line in lines: - product_id.append(line.product_id.id) - product_uom.append(line.product_uom.id) - res.update({'product_ids': product_id,'product_uom':product_uom}) - return res - - for order in self.browse(cr, uid, new_id.keys()): - assert order.state == 'draft',"New created order state should be in draft" - assert order.partner_id == order3.partner_id ,"partner is not correspond" - assert order.location_id == order3.location_id ,"Location is not correspond" - - total_new_qty = sum(x.product_qty for x in order.order_line) - assert total_new_qty == total_qty,"product quantities are not correspond" - - date_order = min(order3.date_order, order7.date_order) - assert order.date_order == date_order, "Date of order is not correspond" - - n_product_data = merged_data(order.order_line) - o_product_data= merged_data(order3.order_line) - o_pro_data = merged_data(order7.order_line) - assert n_product_data == o_product_data or o_pro_data,"product data are not correspond" - diff --git a/addons/purchase/test/process/rfq2order2done.yml b/addons/purchase/test/process/rfq2order2done.yml deleted file mode 100644 index 9098dcc43a1..00000000000 --- a/addons/purchase/test/process/rfq2order2done.yml +++ /dev/null @@ -1,70 +0,0 @@ -- - In order to test the purchase order flow I compute the total of the listed products -- - I check the total untaxed amount of the RFQ is correctly computed -- - !python {model: purchase.order}: | - from openerp.tools import float_compare - po = self.browse(cr, uid, ref('purchase_order_1')) - assert float_compare(sum([l.price_subtotal for l in po.order_line]), po.amount_untaxed, precision_digits=2) == 0, "The amount of RFQ is not correctly computed" -- - I confirm the RFQ. -- - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_1} -- - I check the "Approved" status after confirmed RFQ. -- - !assert {model: purchase.order, id: purchase_order_1}: - - state == 'approved' -- - I check that the invoice details which is generated after confirmed RFQ. -- - !python {model: purchase.order}: | - from openerp.tools import float_compare - purchase_order = self.browse(cr, uid, ref("purchase_order_1")) - assert len(purchase_order.invoice_ids) >= 1, "Invoice is not generated more or less than one" - for invoice in purchase_order.invoice_ids: - assert invoice.state == "draft", "Invoice state should be draft" - assert invoice.partner_id.id == purchase_order.partner_id.id, "Vendor is not correspond with purchase order" - assert invoice.reference == purchase_order.partner_ref or purchase_order.name,"Invoice reference is not correspond with purchase order" - assert invoice.type == 'in_invoice',"Invoice type is not correspond with purchase order" - assert invoice.origin == purchase_order.name,"Invoice origin is not correspond with purchase order" - assert invoice.company_id.id == purchase_order.company_id.id ,"Invoice company is not correspond with purchase order" - assert float_compare(invoice.amount_untaxed, purchase_order.amount_untaxed, precision_digits=2) == 0, "Invoice untaxed amount is not correspond with purchase order" - assert float_compare(invoice.amount_tax, purchase_order.amount_tax, precision_digits=2) == 0, "Invoice tax amount is not correspond with purchase order" - assert float_compare(invoice.amount_total, purchase_order.amount_total, precision_digits=2) == 0, "Invoice total amount is not correspond with purchase order" - assert len(invoice.invoice_line_ids) == len(purchase_order.order_line), "Lines of Invoice and Purchase Order are not correspond" -- - I check that Reception details after confirmed RFQ. -- - !python {model: purchase.order}: | - purchase_order = self.browse(cr, uid, ref("purchase_order_1")) - assert len(purchase_order.picking_ids) >= 1, "You should have only one reception order" - for picking in purchase_order.picking_ids: - assert picking.state == "assigned", "Reception state should be in assigned state" - assert picking.partner_id.id == purchase_order.partner_id.id, "Delivery address of reception id is different from order" - assert picking.company_id.id == purchase_order.company_id.id, "Company is not correspond with purchase order" -- - Reception is ready for process so now done the reception. -- - !python {model: stock.picking}: | - pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_1"), context=context).picking_ids[0] - pick_ids.do_transfer() -- - I check that purchase order is shipped. -- - !python {model: purchase.order}: | - assert self.browse(cr, uid, ref("purchase_order_1")).shipped == True,"Purchase order should be delivered" - -- - I Validate Invoice of Purchase Order. -- - !python {model: purchase.order}: | - for invoice in self.browse(cr, uid, ref('purchase_order_1'), context=context).invoice_ids: - invoice.signal_workflow('invoice_open') -- - I check that purchase order is invoiced. -- - !python {model: purchase.order}: | - assert self.browse(cr, uid, ref("purchase_order_1")).invoiced == True,"Purchase Order should be invoiced" - diff --git a/addons/purchase/tests/__init__.py b/addons/purchase/tests/__init__.py index 15ef7dc32e9..67dee8c60db 100644 --- a/addons/purchase/tests/__init__.py +++ b/addons/purchase/tests/__init__.py @@ -1,5 +1,2 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import test_purchase_to_invoice -from . import test_onchange_product_id diff --git a/addons/purchase/tests/test_onchange_product_id.py b/addons/purchase/tests/test_onchange_product_id.py deleted file mode 100644 index fba548b6212..00000000000 --- a/addons/purchase/tests/test_onchange_product_id.py +++ /dev/null @@ -1,44 +0,0 @@ -from openerp.tests.common import TransactionCase - -class TestOnchangeProductId(TransactionCase): - """Test that when an included tax is mapped by a fiscal position, the included tax must be - subtracted to the price of the product. - """ - - def setUp(self): - super(TestOnchangeProductId, self).setUp() - self.fiscal_position_model = self.registry('account.fiscal.position') - self.fiscal_position_tax_model = self.registry('account.fiscal.position.tax') - self.tax_model = self.registry('account.tax') - self.res_partner_model = self.registry('res.partner') - self.product_model = self.registry('product.product') - self.product_uom_model = self.registry('product.uom') - self.po_line_model = self.registry('purchase.order.line') - - def test_onchange_product_id(self): - cr, uid = self.cr, self.uid - uom_id = self.product_uom_model.search(cr, uid, [('name', '=', 'Unit(s)')])[0] - partner_id = self.res_partner_model.create(cr, uid, dict(name="George")) - tax_include_id = self.tax_model.create(cr, uid, dict(name="Include tax", - amount_type='percent', - amount='21.0', - type_tax_use='purchase', - price_include=True)) - tax_exclude_id = self.tax_model.create(cr, uid, dict(name="Exclude tax", - amount_type='percent', - type_tax_use='purchase', - amount='0.00')) - product_id = self.product_model.create(cr, uid, dict(name="Voiture", - list_price='121', - supplier_taxes_id=[(6, 0, [tax_include_id])])) - fp_id = self.fiscal_position_model.create(cr, uid, dict(name="fiscal position", - sequence=1)) - self.fiscal_position_tax_model.create(cr, uid, dict(position_id=fp_id, - tax_src_id=tax_include_id, - tax_dest_id=tax_exclude_id)) - res = self.po_line_model.onchange_product_id(cr, uid, [], product_id, 1.0, uom_id, partner_id, - fiscal_position_id=fp_id) - - self.assertEquals(0, res['value']['price_unit'], "The default price for products having no seller set is 0") - #TODO: add back a test on this - #self.assertEquals(100, res['value']['price_unit'], "The included tax must be subtracted to the price") diff --git a/addons/purchase/tests/test_purchase_to_invoice.py b/addons/purchase/tests/test_purchase_to_invoice.py deleted file mode 100644 index 5e524bfa8f7..00000000000 --- a/addons/purchase/tests/test_purchase_to_invoice.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- - -from openerp.tests import common - - -class TestPurchase(common.TransactionCase): - - def test_purchase_to_invoice(self): - """ Testing for invoice create,validate and pay with invoicing and payment user.""" - group_id = self.ref('account.group_account_invoice') - product_id = self.ref('product.product_category_3') - company_id = self.ref('base.main_company') - location_id = self.ref('stock.stock_location_3') - - # Useful accounts - user_type_id = self.ref('account.data_account_type_expenses') - account_exp_id = self.env['account.account'].create({'code': 'X2020', 'name': 'Purchase - Test Expense Account', 'user_type_id': user_type_id, 'reconcile': True}) - user_type_id = self.ref('account.data_account_type_payable') - account_pay_id = self.env['account.account'].create({'code': 'X1012', 'name': 'Purchase - Test Payable Account', 'user_type_id': user_type_id, 'reconcile': True}) - - self.env['product.product'].browse(product_id).product_tmpl_id.write({'property_account_expense_id': account_exp_id}) - - # Create Purchase Journal - self.env['account.journal'].create({'name': 'Purchase Journal - Test', 'code': 'PTPJ', 'type': 'purchase'}) - - # In order to test, I create new user and applied Invoicing & Payments group. - user = self.env['res.users'].with_context({'no_reset_password': True}).create({ - 'name': 'Test User', - 'login': 'test@test.com', - 'company_id': company_id, - 'groups_id': [(6, 0, [group_id])]}) - assert user, "User will not created." - # I create partner for purchase order. - partner = self.env['res.partner'].create({ - 'name': 'Test Customer', - 'email': 'testcustomer@test.com', - 'property_account_payable_id': account_pay_id, - }) - - # In order to test I create purchase order and confirmed it. - order = self.env['purchase.order'].create({ - 'partner_id': partner.id, - 'location_id': location_id, }) - self.env['purchase.order.line'].create({ - 'order_id': order.id, - 'product_id': product_id, - 'product_qty': 100.0, - 'product_uom': 1, - 'price_unit': 89.0, - 'name': 'Service', - 'date_planned': '2014-05-31'}) - assert order, "purchase order will not created." - context = {"active_model": 'purchase.order', "active_ids": [order.id], "active_id": order.id} - order.with_context(context).wkf_confirm_order() - # In order to test I create invoice. - invoice = order.with_context(context).action_invoice_create() - assert invoice, "No any invoice is created for this purchase order" - # In order to test I validate invoice wihth Test User(invoicing and payment). - res = self.env['account.invoice'].browse(invoice).with_context(context).invoice_validate() - self.assertTrue(res, 'Invoice will not validated') diff --git a/addons/purchase/views/report_purchaseorder.xml b/addons/purchase/views/report_purchaseorder.xml index f7b5b2c8aa3..c6d089ba34a 100644 --- a/addons/purchase/views/report_purchaseorder.xml +++ b/addons/purchase/views/report_purchaseorder.xml @@ -43,10 +43,6 @@ Order Date:

-

- Validated By: -

-

diff --git a/addons/purchase/wizard/__init__.py b/addons/purchase/wizard/__init__.py deleted file mode 100644 index b12716ddd44..00000000000 --- a/addons/purchase/wizard/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import purchase_order_group -import purchase_line_invoice diff --git a/addons/purchase/wizard/purchase_line_invoice.py b/addons/purchase/wizard/purchase_line_invoice.py deleted file mode 100644 index 4e1d8c96c26..00000000000 --- a/addons/purchase/wizard/purchase_line_invoice.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import osv -from openerp.tools.translate import _ - - -class purchase_line_invoice(osv.osv_memory): - - """ To create invoice for purchase order line""" - - _name = 'purchase.order.line_invoice' - _description = 'Purchase Order Line Make Invoice' - - def _make_invoice_by_partner(self, cr, uid, partner, orders, lines_ids, context=None): - """ - create a new invoice for one vendor - @param cr : Cursor - @param uid : Id of current user - @param partner : The object partner - @param orders : The set of orders to add in the invoice - @param lines : The list of line's id - """ - purchase_obj = self.pool.get('purchase.order') - account_jrnl_obj = self.pool.get('account.journal') - invoice_obj = self.pool.get('account.invoice') - name = orders and orders[0].name or '' - journal_id = account_jrnl_obj\ - .search(cr, uid, [('type', '=', 'purchase')], context=None) - journal_id = journal_id and journal_id[0] or False - a = partner.property_account_payable_id.id - inv = { - 'name': name, - 'origin': name, - 'type': 'in_invoice', - 'journal_id': journal_id, - 'reference': partner.ref, - 'account_id': a, - 'partner_id': partner.id, - 'invoice_line_ids': [(6, 0, lines_ids)], - 'currency_id': orders[0].currency_id.id, - 'comment': " \n".join([order.notes for order in orders if order.notes]), - 'payment_term_id': orders[0].payment_term_id.id, - 'fiscal_position_id': partner.property_account_position_id.id - } - inv_id = invoice_obj.create(cr, uid, inv, context=context) - purchase_obj.write(cr, uid, [order.id for order in orders], {'invoice_ids': [(4, inv_id)]}, context=context) - return inv_id - - def makeInvoices(self, cr, uid, ids, context=None): - - """ - To get Purchase Order line and create Invoice - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param context: A standard dictionary - @return : retrun view of Invoice - """ - - if context is None: - context={} - - record_ids = context.get('active_ids',[]) - if record_ids: - res = False - invoices = {} - purchase_obj = self.pool.get('purchase.order') - purchase_line_obj = self.pool.get('purchase.order.line') - invoice_line_obj = self.pool.get('account.invoice.line') - - for line in purchase_line_obj.browse(cr, uid, record_ids, context=context): - if (not line.invoiced) and (line.state not in ('draft', 'cancel')): - if not line.partner_id.id in invoices: - invoices[line.partner_id.id] = [] - acc_id = purchase_obj._choose_account_from_po_line(cr, uid, line, context=context) - inv_line_data = purchase_obj._prepare_inv_line(cr, uid, acc_id, line, context=context) - inv_line_data.update({'origin': line.order_id.name}) - inv_id = invoice_line_obj.create(cr, uid, inv_line_data, context=context) - purchase_line_obj.write(cr, uid, [line.id], {'invoiced': True, 'invoice_lines': [(4, inv_id)]}) - invoices[line.partner_id.id].append((line,inv_id)) - - res = [] - for result in invoices.values(): - il = map(lambda x: x[1], result) - orders = list(set(map(lambda x : x[0].order_id, result))) - - res.append(self._make_invoice_by_partner(cr, uid, orders[0].partner_id, orders, il, context=context)) - - return { - 'domain': "[('id','in', ["+','.join(map(str,res))+"])]", - 'name': _('Vendor Bills'), - 'view_type': 'form', - 'view_mode': 'tree,form', - 'res_model': 'account.invoice', - 'view_id': False, - 'context': "{'type':'in_invoice', 'journal_type': 'purchase'}", - 'type': 'ir.actions.act_window' - } diff --git a/addons/purchase/wizard/purchase_line_invoice_view.xml b/addons/purchase/wizard/purchase_line_invoice_view.xml deleted file mode 100644 index 8af82622f4a..00000000000 --- a/addons/purchase/wizard/purchase_line_invoice_view.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - Create invoices - purchase.order.line_invoice - -
- - -
-
- -
-
- - -
-
- diff --git a/addons/purchase/wizard/purchase_order_group.py b/addons/purchase/wizard/purchase_order_group.py deleted file mode 100644 index 7c7f805cc68..00000000000 --- a/addons/purchase/wizard/purchase_order_group.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import time - -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError - -class purchase_order_group(osv.osv_memory): - _name = "purchase.order.group" - _description = "Purchase Order Merge" - - def fields_view_get(self, cr, uid, view_id=None, view_type='form', - context=None, toolbar=False, submenu=False): - """ - Changes the view dynamically - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param context: A standard dictionary - @return: New arch of view. - """ - if context is None: - context={} - res = super(purchase_order_group, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar,submenu=False) - if context.get('active_model','') == 'purchase.order' and len(context['active_ids']) < 2: - raise UserError(_('Please select multiple order to merge in the list view.')) - return res - def merge_orders(self, cr, uid, ids, context=None): - """ - To merge similar type of purchase orders. - - @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: purchase order view - - """ - order_obj = self.pool.get('purchase.order') - proc_obj = self.pool.get('procurement.order') - mod_obj =self.pool.get('ir.model.data') - if context is None: - context = {} - result = mod_obj._get_id(cr, uid, 'purchase', 'view_purchase_order_filter') - id = mod_obj.read(cr, uid, result, ['res_id']) - - allorders = order_obj.do_merge(cr, uid, context.get('active_ids',[]), context) - - return { - 'domain': "[('id','in', [" + ','.join(map(str, allorders.keys())) + "])]", - 'name': _('Purchase Orders'), - 'view_type': 'form', - 'view_mode': 'tree,form', - 'res_model': 'purchase.order', - 'view_id': False, - 'type': 'ir.actions.act_window', - 'search_view_id': id['res_id'] - } diff --git a/addons/purchase/wizard/purchase_order_group_view.xml b/addons/purchase/wizard/purchase_order_group_view.xml deleted file mode 100644 index d0ee1bd3ad8..00000000000 --- a/addons/purchase/wizard/purchase_order_group_view.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Merger Purchase Orders - purchase.order.group - -
- - - - -
-
- -
-
- - - -
-
-