From 14f973be4e944d1ebe7e2c144acc585515ef2a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 6 May 2008 17:45:44 +0200 Subject: [PATCH] Initial commit of purchase module --- __init__.py | 27 ++ __tryton__.py | 23 ++ purchase.py | 866 ++++++++++++++++++++++++++++++++++++++++++++++++++ purchase.xml | 647 +++++++++++++++++++++++++++++++++++++ setup.py | 53 +++ 5 files changed, 1616 insertions(+) create mode 100644 __init__.py create mode 100644 __tryton__.py create mode 100644 purchase.py create mode 100644 purchase.xml create mode 100644 setup.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..563fd3eae0 --- /dev/null +++ b/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2004-2006 Tiny SPRL. (http://tiny.be) +# Copyright (c) 2007-2008 Cedric Krier. +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contact a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from purchase import * diff --git a/__tryton__.py b/__tryton__.py new file mode 100644 index 0000000000..f39491dbe6 --- /dev/null +++ b/__tryton__.py @@ -0,0 +1,23 @@ +{ + 'name': 'Purchase', + 'version': '0.1', + 'author': 'B2CK', + 'email': 'info@b2ck.com', + 'website': 'http://www.tryton.org/', + 'category': 'Generic Modules', + 'description': 'Purchase Management', + 'depends': [ + 'company', + 'partner', + 'stock', + 'account', + 'product', + 'account_invoice', + 'workflow', + 'res', + 'ir', + ], + 'xml': [ + 'purchase.xml', + ], +} diff --git a/purchase.py b/purchase.py new file mode 100644 index 0000000000..a46ba3ce80 --- /dev/null +++ b/purchase.py @@ -0,0 +1,866 @@ +"Purchase" + +from trytond.osv import fields, OSV +from decimal import Decimal +import datetime +from trytond.netsvc import LocalService + +_STATES = { + 'readonly': "state != 'draft'", +} + + +class Purchase(OSV): + 'Purchase' + _name = 'purchase.purchase' + _description = __doc__ + + company = fields.Many2One('company.company', 'Company', required=True, + states=_STATES) + reference = fields.Char('Reference', size=None, readonly=True, select=1) + description = fields.Char('Description', size=None, states=_STATES) + state = fields.Selection([ + ('draft', 'Draft'), + ('quotation', 'Quotation'), + ('confirmed', 'Confirmed'), + ('done', 'Done'), + ('cancel', 'Cancel'), + ], 'State', readonly=True, required=True) + purchase_date = fields.Date('Purchase Date', required=True, states=_STATES) + payment_term = fields.Many2One('account.invoice.payment_term', + 'Payment Term', required=True, states=_STATES) + partner = fields.Many2One('partner.partner', 'Partner', change_default=True, + required=True, states=_STATES, on_change=['partner', 'payment_term']) + contact_address = fields.Many2One('partner.address', 'Contact Address', + domain="[('partner', '=', partner)]", states=_STATES) + invoice_address = fields.Many2One('partner.address', 'Invoice Address', + domain="[('partner', '=', partner)]", states=_STATES) + warehouse = fields.Many2One('stock.location', 'Warehouse', + domain=[('type', '=', 'warehouse')], required=True, states=_STATES) + currency = fields.Many2One('account.currency', 'Currency', required=True, + states=_STATES) + lines = fields.One2Many('purchase.line', 'purchase', 'Lines', + states=_STATES) + comment = fields.Text('Comment') + #TODO digits must depend of currency + untaxed_amount = fields.Function('get_untaxed_amount', type='numeric', + digits=(16, 2), string='Untaxed') + tax_amount = fields.Function('get_tax_amount', type='numeric', + digits=(16, 2), string='Tax') + total_amount = fields.Function('get_total_amount', type='numeric', + digits=(16, 2), string='Total') + invoice_method = fields.Selection([ + ('manual', 'Manual'), + ('order', 'From Order'), + ('packing', 'From Packing'), + ], 'Invoice Method', required=True, states=_STATES) + invoice_state = fields.Selection([ + ('none', 'None'), + ('waiting', 'Waiting'), + ('paid', 'Paid'), + ('exception', 'Exception'), + ], 'Invoice State', readonly=True, required=True) + invoices = fields.Many2Many('account.invoice', 'purchase_invoices_rel', + 'purchase', 'invoice', 'Invoices', readonly=True) + invoices_ignored = fields.Many2Many('account.invoice', + 'purchase_invoice_ignored_rel', 'purchase', 'invoice', + 'Invoices Ignored', readonly=True) + invoice_paid = fields.Function('get_invoice_paid', type='boolean', + string='Invoices Paid') + invoice_exception = fields.Function('get_invoice_exception', type='boolean', + string='Invoices Exception') + packing_state = fields.Selection([ + ('none', 'None'), + ('waiting', 'Waiting'), + ('received', 'Received'), + ('exception', 'Exception'), + ], 'Packing State', readonly=True, required=True) + packings = fields.Function('get_packings', type='many2many', + relation='stock.packing.in', string='Packings') + moves = fields.Function('get_moves', type='many2many', + relation='stock.move', string='Moves') + packing_done = fields.Function('get_packing_done', type='boolean', + string='Packing Done') + packing_exception = fields.Function('get_packing_exception', type='boolean', + string='Packings Exception') + + def __init__(self): + super(Purchase, self).__init__() + self._rpc_allowed += [ + 'button_compute', + ] + + def default_company(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + if context is None: + context = {} + if context.get('company'): + return company_obj.name_get(cursor, user, context['company'], + context=context)[0] + return False + + def default_state(self, cursor, user, context=None): + return 'draft' + + def default_purchase_date(self, cursor, user, context=None): + return datetime.date.today() + + def default_currency(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + currency_obj = self.pool.get('account.currency') + if context is None: + context = {} + company = None + if context.get('company'): + company = company_obj.browse(cursor, user, context['company'], + context=context) + return currency_obj.name_get(cursor, user, company.currency.id, + context=context)[0] + return False + + def default_invoice_method(self, cursor, user, context=None): + return 'order' + + def default_invoice_state(self, cursor, user, context=None): + return 'none' + + def default_packing_state(self, cursor, user, context=None): + return 'none' + + def on_change_partner(self, cursor, user, ids, vals, context=None): + partner_obj = self.pool.get('partner.partner') + address_obj = self.pool.get('partner.address') + payment_term_obj = self.pool.get('account.invoice.payment_term') + res = { + 'invoice_address': False, + 'contact_address': False, + 'payment_term': False, + } + if vals.get('partner'): + partner = partner_obj.browse(cursor, user, vals['partner'], + context=context) + res['contact_address'] = partner_obj.address_get(cursor, user, + partner.id, type=None, context=context) + res['invoice_address'] = partner_obj.address_get(cursor, user, + partner.id, type='invoice', context=context) + if partner.payment_term: + res['payment_term'] = partner.payment_term.id + + if res['contact_address']: + res['contact_address'] = address_obj.name_get(cursor, user, + res['contact_address'], context=context)[0] + if res['invoice_address']: + res['invoice_address'] = address_obj.name_get(cursor, user, + res['invoice_address'], context=context)[0] + if res['payment_term']: + res['payment_term'] = payment_term_obj.name_get(cursor, user, + res['payment_term'], context=context)[0] + return res + + def get_untaxed_amount(self, cursor, user, ids, name, arg, context=None): + currency_obj = self.pool.get('account.currency') + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + res.setdefault(purchase.id, Decimal('0.0')) + for line in purchase.lines: + if line.type != 'line': + continue + res[purchase.id] += line.amount + res[purchase.id] = currency_obj.round(cursor, user, purchase.currency, + res[purchase.id]) + return res + + def get_tax_context(self, cursor, user, purchase, context=None): + res = {} + if purchase.partner.lang: + res['language'] = purchase.partner.lang.code + return res + + def get_tax_amount(self, cursor, user, ids, name, arg, context=None): + currency_obj = self.pool.get('account.currency') + tax_obj = self.pool.get('account.tax') + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + ctx = context.copy() + ctx.update(self.get_tax_context(cursor, user, + purchase, context=context)) + res.setdefault(purchase.id, Decimal('0.0')) + for line in purchase.lines: + for tax in tax_obj.compute(cursor, user, line.taxes, + line.unit_price, line.quantity, context=ctx): + res[purchase.id] += tax['amount'] + res[purchase.id] = currency_obj.round(cursor, user, purchase.currency, + res[purchase.id]) + res[purchase.id] = currency_obj.round(cursor, user, purchase.currency, + res[purchase.id]) + return res + + def get_total_amount(self, cursor, user, ids, name, arg, context=None): + currency_obj = self.pool.get('account.currency') + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + res[purchase.id] = currency_obj.round(cursor, user, purchase.currency, + purchase.untaxed_amount + purchase.tax_amount) + return res + + def get_invoice_paid(self, cursor, user, ids, name, args, context=None): + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + val = True + ignored_ids = [x.id for x in purchase.invoices_ignored] + for invoice in purchase.invoices: + if invoice.state != 'paid' \ + and invoice.id not in ignored_ids: + val = False + break + res[purchase.id] = val + return res + + def get_invoice_exception(self, cursor, user, ids, name, args, context=None): + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + val = False + ignored_ids = [x.id for x in purchase.invoices_ignored] + for invoice in purchase.invoices: + if invoice.state == 'cancel' \ + and invoice.id not in ignored_ids: + val = True + break + res[purchase.id] = val + return res + + def get_packings(self, cursor, user, ids, name, args, context=None): + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + res[purchase.id] = [] + for line in purchase.lines: + for move in line.moves: + if move.incoming_packing_in: + if move.incoming_packing_in.id not in res[purchase.id]: + res[purchase.id].append(move.incoming_packing_in.id) + return res + + def get_moves(self, cursor, user, ids, name, args, context=None): + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + res[purchase.id] = [] + for line in purchase.lines: + res[purchase.id].extend([x.id for x in line.moves]) + return res + + def get_packing_done(self, cursor, user, ids, name, args, context=None): + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + val = True + for line in purchase.lines: + if not line.move_done: + val = False + break + res[purchase.id] = val + return res + + def get_packing_exception(self, cursor, user, ids, name, args, + context=None): + res = {} + for purchase in self.browse(cursor, user, ids, context=context): + val = False + ignored_ids = [] + for line in purchase.lines: + if line.move_exception: + val = True + break + res[purchase.id] = val + return res + + def name_get(self, cursor, user, ids, context=None): + if not ids: + return [] + if isinstance(ids, (int, long)): + ids = [ids] + res = [] + for purchase in self.browse(cursor, user, ids, context=context): + res.append((purchase.id, purchase.reference or '/' + ' ' \ + + purchase.partner.name)) + return res + + def name_search(self, cursor, user, name='', args=None, operator='ilike', + context=None, limit=None): + if args is None: + args = [] + if name: + ids = self.search(cursor, user, [('reference', operator, name)] + args, + limit=limit, context=context) + if not ids: + ids = self.search(cursor, user, [('partner', operator, name)] + args, + limit=limit, context=context) + res = self.name_get(cursor, user, ids, context=context) + return res + + def copy(self, cursor, user, purchase_id, default=None, context=None): + if default is None: + default = {} + default = default.copy() + default['state'] = 'draft' + default['reference'] = False + default['invoice_state'] = 'none' + default['invoices'] = False + default['invoices_ignored'] = False + default['packing_state'] = 'none' + return super(Purchase, self).copy(cursor, user, purchase_id, + default=default, context=context) + + def button_compute(self, cursor, user, ids, context=None): + pass + + def set_reference(self, cursor, user, purchase_id, context=None): + sequence_obj = self.pool.get('ir.sequence') + + purchase = self.browse(cursor, user, purchase_id, context=context) + + if purchase.reference: + return True + + reference = sequence_obj.get(cursor, user, 'purchase.purchase') + self.write(cursor, user, purchase_id, { + 'reference': reference, + }, context=context) + return True + + def _get_invoice_line_purchase_line(self, cursor, user, purchase, + context=None): + ''' + Return list of invoice line values for each purchase lines + ''' + line_obj = self.pool.get('purchase.line') + res = {} + for line in purchase.lines: + val = line_obj.get_invoice_line(cursor, user, line, + context=context) + if val: + res[line.id] = val + return res + + def create_invoice(self, cursor, user, purchase_id, context=None): + invoice_obj = self.pool.get('account.invoice') + journal_obj = self.pool.get('account.journal') + invoice_line_obj = self.pool.get('account.invoice.line') + purchase_line_obj = self.pool.get('purchase.line') + + purchase = self.browse(cursor, user, purchase_id, context=context) + + invoice_lines = self._get_invoice_line_purchase_line(cursor, user, + purchase, context=context) + if not invoice_lines: + return + + journal_id = journal_obj.search(cursor, user, [ + ('type', '=', 'expense'), + ], limit=1, context=context) + if journal_id: + journal_id = journal_id[0] + + invoice_id = invoice_obj.create(cursor, user, { + 'company': purchase.company.id, + 'type': 'in_invoice', + 'reference': purchase.reference, + 'journal': journal_id, + 'partner': purchase.partner.id, + 'contact_address': purchase.contact_address.id, + 'invoice_address': purchase.invoice_address.id, + 'currency': purchase.currency.id, + 'account': purchase.partner.account_payable.id, + 'payment_term': purchase.payment_term.id, + }, context=context) + + for line_id in invoice_lines: + vals = invoice_lines[line_id] + vals['invoice'] = invoice_id + invoice_line_id = invoice_line_obj.create(cursor, user, vals, + context=context) + purchase_line_obj.write(cursor, user, line_id, { + 'invoice_lines': [('add', invoice_line_id)], + }, context=context) + + invoice_obj.button_compute(cursor, user, [invoice_id], context=context) + + self.write(cursor, user, purchase_id, { + 'invoices': [('add', invoice_id)], + }, context=context) + return invoice_id + + def ignore_invoice_exception(self, cursor, user, purchase_id, context=None): + purchase = self.browse(cursor, user, purchase_id, context=context) + invoice_ids = [] + for invoice in purchase.invoices: + if invoice.state == 'cancel': + invoice_ids.append(invoice.id) + if invoice_ids: + self.write(cursor, user, purchase_id, { + 'invoices_ignored': [('add', x) for x in invoice_ids], + }, context=context) + + def create_move(self, cursor, user, purchase_id, context=None): + ''' + Create move for each purchase lines + ''' + line_obj = self.pool.get('purchase.line') + + purchase = self.browse(cursor, user, purchase_id, context=context) + for line in purchase.lines: + line_obj.create_move(cursor, user, line, context=context) + + def ignore_packing_exception(self, cursor, user, purchase_id, context=None): + line_obj = self.pool.get('purchase.line') + + purchase = self.browse(cursor, user, purchase_id, context=context) + for line in purchase.lines: + line_obj.ignore_move_exception(cursor, user, line, context=context) + +Purchase() + + +class PurchaseLine(OSV): + 'Purchase Line' + _name = 'purchase.line' + _rec_name = 'description' + _description = __doc__ + + purchase = fields.Many2One('purchase.purchase', 'Purchase', ondelete='CASCADE', + select=1) + sequence = fields.Integer('Sequence') + type = fields.Selection([ + ('line', 'Line'), + ('subtotal', 'Subtotal'), + ('title', 'Title'), + ], 'Type', select=1, required=True) + quantity = fields.Float('Quantity', + states={ + 'invisible': "type != 'line'", + 'required': "type == 'line'", + }) + unit = fields.Many2One('product.uom', 'Unit', + states={ + 'required': "product", + 'invisible': "type != 'line'", + }, domain="[('category', '=', (product, 'product'))]") + product = fields.Many2One('product.product', 'Product', + states={ + 'invisible': "type != 'line'", + }, on_change=['product', 'unit', 'quantity', 'description', + 'parent.partner', 'parent.currency']) + unit_price = fields.Numeric('Unit Price', digits=(16, 4), + states={ + 'invisible': "type != 'line'", + 'required': "type == 'line'", + }) + amount = fields.Function('get_amount', type='numeric', string='Amount', + states={ + 'invisible': "type not in ('line', 'subtotal')", + }) + description = fields.Char('Description', size=None, required=True) + comment = fields.Text('Comment', + states={ + 'invisible': "type != 'line'", + }) + taxes = fields.Many2Many('account.tax', 'purchase_line_account_tax', + 'line', 'tax', 'Taxes', domain=[('parent', '=', False)], + states={ + 'invisible': "type != 'line'", + }) + invoice_lines = fields.Many2Many('account.invoice.line', + 'purchase_line_invoice_lines_rel', 'purchase_line', 'invoice_line', + 'Invoice Lines', readonly=True) + moves = fields.Many2Many('stock.move', 'purchase_line_moves_rel', + 'purchase_line', 'move', 'Moves', readonly=True) + moves_ignored = fields.Many2Many('stock.move', 'purchase_line_moves_ignored_rel', + 'purchase_line', 'move', 'Moves Ignored', readonly=True) + move_done = fields.Function('get_move_done', type='boolean', + string='Moves Done') + move_exception = fields.Function('get_move_exception', type='boolean', + string='Moves Exception') + + def __init__(self): + super(PurchaseLine, self).__init__() + self._order.insert(0, ('sequence', 'ASC')) + + def default_type(self, cursor, user, context=None): + return 'line' + + def default_quantity(self, cursor, user, context=None): + return 0.0 + + def default_unit_price(self, cursor, user, context=None): + return Decimal('0.0') + + def get_move_done(self, cursor, user, ids, name, args, context=None): + uom_obj = self.pool.get('product.uom') + res = {} + for line in self.browse(cursor, user, ids, context=context): + val = True + ignored_ids = [x.id for x in line.moves_ignored] + quantity = line.quantity + for move in line.moves: + if move.state != 'done' \ + and move.id not in ignored_ids: + val = False + break + quantity -= uom_obj.compute_qty(cursor, user, move.uom, + move.quantity, line.unit) + if val: + if quantity > 0.0: + val = False + res[line.id] = val + return res + + def get_move_exception(self, cursor, user, ids, name, args, context=None): + res = {} + for line in self.browse(cursor, user, ids, context=context): + val = False + ignored_ids = [x.id for x in line.moves_ignored] + for move in line.moves: + if move.state == 'cancel' \ + and move.id not in ignored_ids: + val = True + break + res[line.id] = val + return res + + def on_change_product(self, cursor, user, ids, vals, context=None): + partner_obj = self.pool.get('partner.partner') + product_obj = self.pool.get('product.product') + uom_obj = self.pool.get('product.uom') + if context is None: + context = {} + if not vals.get('product'): + return {} + res = {} + + ctx = context.copy() + partner = None + if vals.get('parent.partner'): + partner = partner_obj.browse(cursor, user, vals['parent.partner'], + context=context) + if partner.lang: + ctx['language'] = partner.lang.code + + product = product_obj.browse(cursor, user, vals['product'], + context=context) + + ctx2 = context.copy() + if vals.get('parent.currency'): + ctx2['currency'] = vals['parent.currency'] + if vals.get('parent.partner'): + ctx2['supplier'] = vals['parent.partner'] + if vals.get('unit'): + ctx2['uom'] = vals['unit'] + res['unit_price'] = product_obj.get_purchase_price(cursor, user, + [product.id], vals.get('quantity', 0), context=ctx2)[product.id] + res['taxes'] = [] + for tax in product.supplier_taxes: + if partner: + if partner.get(tax.group.code): + res['taxes'].append(partner.get(tax.group.code).id) + continue + res['taxes'].append(tax.id) + + if not vals.get('description'): + res['description'] = product_obj.name_get(cursor, user, product.id, + context=ctx)[0][1] + + category = product.default_uom.category + if not vals.get('unit') \ + or vals.get('unit') not in [x.id for x in category.uoms]: + res['unit'] = uom_obj.name_get(cursor, user, product.default_uom.id, + context=context)[0] + return res + + def get_amount(self, cursor, user, ids, name, arg, context=None): + currency_obj = self.pool.get('account.currency') + res = {} + for line in self.browse(cursor, user, ids, context=context): + if line.type == 'line': + res[line.id] = currency_obj.round(cursor, user, + line.purchase.currency, + Decimal(str(line.quantity)) * line.unit_price) + elif line.type == 'subtotal': + res[line.id] = Decimal('0.0') + for line2 in line.purchase.lines: + if line2.type == 'line': + res[line.id] += currency_obj.round(cursor, user, + line2.purchase.currency, + Decimal(str(line2.quantity)) * line2.unit_price) + elif line2.type == 'subtotal': + if line.id == line2.id: + break + res[line.id] = Decimal('0.0') + else: + res[line.id] = Decimal('0.0') + return res + + def get_invoice_line(self, cursor, user, line, context=None): + ''' + Return invoice line values for purchase line + ''' + uom_obj = self.pool.get('product.uom') + + res = {} + res['sequence'] = line.sequence + res['type'] = line.type + res['description'] = line.description + if line.type != 'line': + return res + if line.purchase.invoice_method == 'order': + res['quantity'] = line.quantity + else: + quantity = 0.0 + for move in line.moves: + if move.state == 'done': + quantity += uom_obj.compute_qty(cursor, user, move.uom, + move.quantity, line.unit) + for invoice_line in line.invoice_lines: + quantity -= uom_obj.compute_qty(cursor, user, + invoice_line.unit, invoice_line.quantity, line.unit) + res['quantity'] = quantity + if res['quantity'] <= 0.0: + return None + res['unit'] = line.unit.id + res['product'] = line.product.id + res['unit_price'] = line.unit_price + res['taxes'] = [('set', [x.id for x in line.taxes])] + res['account'] = line.product.account_expense_used.id + return res + + def copy(self, cursor, user, line_id, default=None, context=None): + if default is None: + default = {} + default = default.copy() + default['moves'] = False + default['moves_ignored'] = False + default['invoice_lines'] = False + return super(PurchaseLine, self).copy(cursor, user, line_id, + default=default, context=context) + + def create_move(self, cursor, user, line, context=None): + ''' + Create move line + ''' + move_obj = self.pool.get('stock.move') + uom_obj = self.pool.get('product.uom') + + vals = {} + if line.type != 'line': + return + quantity = line.quantity + for move in line.moves: + quantity -= uom_obj.compute_qty(cursor, user, move.uom, + move.quantity, line.unit) + if quantity <= 0.0: + return + vals['quantity'] = quantity + vals['uom'] = line.unit + vals['product'] = line.product.id + vals['from_location'] = line.purchase.partner.supplier_location.id + vals['to_location'] = line.purchase.warehouse.input_location.id + vals['state'] = 'waiting' + + move_id = move_obj.create(cursor, user, vals, context=context) + + self.write(cursor, user, line.id, { + 'moves': [('add', move_id)], + }, context=context) + return move_id + + def ignore_move_exception(self, cursor, user, line, context=None): + move_ids = [] + for move in line.moves: + if move.state == 'cancel': + move_ids.append(move.id) + if move_ids: + self.write(cursor, user, line.id, { + 'moves_ignored': [('add', x) for x in move_ids], + }, context=context) + +PurchaseLine() + + +class Template(OSV): + _name = "product.template" + + purchasable = fields.Boolean('Purchasable') + product_suppliers = fields.One2Many('purchase.product_supplier', + 'product', 'Suppliers', states={ + 'invisible': "not purchasable", + }) + + def default_purchasable(self, cursor, user, context=None): + return True + +Template() + + +class Product(OSV): + _name = 'product.product' + + def get_purchase_price(self, cursor, user, ids, quantity=0, context=None): + ''' + Return price for product ids. + context can have : + - uom + - supplier + - currency + ''' + uom_obj = self.pool.get('product.uom') + user_obj = self.pool.get('res.user') + currency_obj = self.pool.get('account.currency') + res = {} + for product in self.browse(cursor, user, ids, context=context): + res[product.id] = product.cost_price + if context.get('supplier') and product.product_suppliers: + supplier_id = context['supplier'] + for product_supplier in product.product_suppliers: + if product_supplier.partner.id == supplier_id: + for price in product_supplier.prices: + if price.quantity <= quantity: + res[product.id] = price.price + break + if context.get('uom'): + uom = uom_obj.browse(cursor, user, context['uom'], + context=context) + res[product.id] = uom_obj.compute_price(cursor, + user, product.default_uom, res[product.id], + uom) + if context.get('currency'): + currency = currency_obj.browse(cursor, user, + context['currency'], context=context) + user2 = user_obj.browse(cursor, user, user, context=context) + if user2.company.currency.id != currency.id: + res[product.id] = currency_obj.compute(cursor, user, + user2.company.currency, res[product.id], + currency, context=context) + return res + +Product() + + +class ProductSupplier(OSV): + 'Product Supplier' + _name = 'purchase.product_supplier' + _description = __doc__ + + product = fields.Many2One('product.template', 'Product', required=True, + ondelete='CASCADE', select=1) + partner = fields.Many2One('partner.partner', 'Partner', required=True, + ondelete='CASCADE', select=1) + name = fields.Char('Name', size=None, translate=True, select=1) + code = fields.Char('Code', size=None, select=1) + sequence = fields.Integer('Sequence') + prices = fields.One2Many('purchase.product_supplier.price', + 'product_supplier', 'Prices') + company = fields.Many2One('company.company', 'Company', required=True, + ondelete='CASCADE', select=1) + + def __init__(self): + super(ProductSupplier, self).__init__() + self._order.insert(0, ('sequence', 'ASC')) + + def default_company(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + if context is None: + context = {} + if context.get('company'): + return company_obj.name_get(cursor, user, context['company'], + context=context)[0] + return False + +ProductSupplier() + + +class ProductSupplierPrice(OSV): + 'Product Supplier Price' + _name = 'purchase.product_supplier.price' + + product_supplier = fields.Many2One('purchase.product_supplier', + 'Supplier', required=True, ondelete='CASCADE') + quantity = fields.Float('Quantity', required=True, help='Minimal quantity') + price = fields.Numeric('Price', required=True, digits=(16, 4)) + + def __init__(self): + super(ProductSupplierPrice, self).__init__() + self._order.insert(0, ('quantity', 'ASC')) + + def default_currency(self, cursor, user, context=None): + company_obj = self.pool.get('company.company') + currency_obj = self.pool.get('account.currency') + if context is None: + context = {} + company = None + if context.get('company'): + company = company_obj.browse(cursor, user, context['company'], + context=context) + return currency_obj.name_get(cursor, user, company.currency.id, + context=context)[0] + return False + +ProductSupplierPrice() + + +class PackingIn(OSV): + _name = 'stock.packing.in' + + def write(self, cursor, user, ids, vals, context=None): + workflow_service = LocalService('workflow') + purchase_line_obj = self.pool.get('purchase.line') + + res = super(PackingIn, self).write(cursor, user, ids, vals, + context=context) + + if 'state' in vals and vals['state'] in ('received', 'cancel'): + purchase_ids = [] + move_ids = [] + if isinstance(ids, (int, long)): + ids = [ids] + for packing in self.browse(cursor, user, ids, context=context): + move_ids.extend([x.id for x in packing.incoming_moves]) + + purchase_line_ids = purchase_line_obj.search(cursor, user, [ + ('moves', 'in', move_ids), + ], context=context) + if purchase_line_ids: + for purchase_line in purchase_line_obj.browse(cursor, user, + purchase_line_ids, context=context): + if purchase_line.purchase.id not in purchase_ids: + purchase_ids.append(purchase_line.purchase.id) + + for purchase_id in purchase_ids: + workflow_service.trg_validate(user, 'purchase.purchase', + purchase_id, 'packing_update', cursor) + return res + +PackingIn() + + +class Move(OSV): + _name = 'stock.move' + + def write(self, cursor, user, ids, vals, context=None): + workflow_service = LocalService('workflow') + purchase_line_obj = self.pool.get('purchase.line') + + res = super(Move, self).write(cursor, user, ids, vals, + context=context) + if 'state' in vals and vals['state'] in ('cancel',): + purchase_ids = [] + purchase_line_ids = purchase_line_obj.search(cursor, user, [ + ('moves', 'in', ids), + ], context=context) + if purchase_line_ids: + for purchase_line in purchase_line_obj.browse(cursor, user, + purchase_line_ids, context=context): + if purchase_line.purchase.id not in purchase_ids: + purchase_ids.append(purchase_line.purchase.id) + for purchase_id in purchase_ids: + workflow_service.trg_validate(user, 'purchase.purchase', + purchase_id, 'packing_update', cursor) + return res + +Move() diff --git a/purchase.xml b/purchase.xml new file mode 100644 index 0000000000..a12c671516 --- /dev/null +++ b/purchase.xml @@ -0,0 +1,647 @@ + + + + + + Purchase + + + + purchase.purchase + form + + +