diff --git a/purchase_collective/__init__.py b/purchase_collective/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/purchase_collective/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/purchase_collective/__manifest__.py b/purchase_collective/__manifest__.py new file mode 100644 index 0000000..7139362 --- /dev/null +++ b/purchase_collective/__manifest__.py @@ -0,0 +1,37 @@ +{ + 'name': "Collective Purchases", + 'summary': "Compras colectivas", + 'description': """ +Compras Colectivas +================================================== +Éste módulo permite a varios usuarios de Odoo realizar una compra de productos a un mismo proveedor que acepta pedidos conjuntos. +El Usuario que crea la orden es el encargado de distribuir los productos al resto de usuarios, cobrar de ellos y pagar al productor. + +- modelo CollectivePurchases, con un campo que linka diferentes sales orders +- crea un único albarán de salida al proveedor con la suma de todas las sale orders +- cuando se recibe el cargamente del proveedor, una acción permite crear albaranes de entrega a los almacenes dde los nodo + --> ¿poner en un módulo aparte y quitar la dependencia a network_partner? +- nueva secuencia para collective order purchase +- workflow propio, por añadir los estados de entrega en subalmacén y entrega ala cliente. +- reglas de seguridad y grupos + """, + 'author': "Criptomart", + 'website': "https://criptomart.net", + 'category': 'Purchase Management', + 'version': '0.12.0.1.0', + 'depends': ["purchase","sale","product","network_partner"], + 'data': [ + 'security/purchase_collective_security.xml', + 'security/ir.model.access.csv', + #'data/purchase_collective_workflow.xml', + #'data/purchase_collective_sequence.xml', + 'views/purchase_collective.xml', + 'views/sale_order.xml', + 'views/actions.xml', + 'views/menus.xml', + ], + 'demo': [], + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/purchase_collective/data/purchase_collective_sequence.xml b/purchase_collective/data/purchase_collective_sequence.xml new file mode 100644 index 0000000..021a5d7 --- /dev/null +++ b/purchase_collective/data/purchase_collective_sequence.xml @@ -0,0 +1,17 @@ + + + + + + Collective Purchase Order + purchase.collective.order + + + Collective Purchase Order + purchase.collective.order + CPO + 5 + + + + diff --git a/purchase_collective/data/purchase_collective_workflow.xml b/purchase_collective/data/purchase_collective_workflow.xml new file mode 100644 index 0000000..7196b2e --- /dev/null +++ b/purchase_collective/data/purchase_collective_workflow.xml @@ -0,0 +1,240 @@ + + + + + + Collective Purchase Order Basic Workflow + purchase.collective.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/purchase_collective/models/__init__.py b/purchase_collective/models/__init__.py new file mode 100644 index 0000000..e45f3e3 --- /dev/null +++ b/purchase_collective/models/__init__.py @@ -0,0 +1,3 @@ +from . import purchase_collective +from . import sale_order + diff --git a/purchase_collective/models/purchase_collective.py b/purchase_collective/models/purchase_collective.py new file mode 100644 index 0000000..6472888 --- /dev/null +++ b/purchase_collective/models/purchase_collective.py @@ -0,0 +1,254 @@ +import pytz +import logging +from datetime import datetime +from dateutil.relativedelta import relativedelta +from operator import attrgetter + +from odoo import models, fields, api, osv, _ +from odoo.exceptions import Warning +from odoo.tools.safe_eval import safe_eval as eval +import odoo.addons.decimal_precision as dp + +_logger = logging.getLogger(__name__) + +class PurchaseCollectiveOrder(models.Model): + _name = 'purchase_collective.order' + _inherit = ['purchase.order'] + _description = 'Collective Purchases' + + sales_order_lines = fields.One2many('sale.order','cp_order_id','Collective Order Lines',states={'approved':[('readonly',True)],'done':[('readonly',True)]},copy=True ) + deadline_date = fields.Date(string='Order Deadline', required=True, help="End date of the order. Place your orders before this date") + amount_untaxed = fields.Float('Amount untaxed') + amount_tax = fields.Float('Taxes') + amount_total = fields.Float('Amount Total') + wh_id = fields.Many2one('stock.warehouse',string="Almacén asociado",help="El almacén central de distribución.") + qty_min =fields.Float('Cantidad mínima del pedido', required=True, help='Cantidad mínima del pedido requerida para ejecutar la orden de compra') + + #@api.onchange('sales_order_lines') + def onchange_order_line(self, args=None): + _logger.info("onchange_order_lines -- args: %s" %(args)) + res = { + 'amount_untaxed': 0.0, + 'amount_tax': 0.0, + 'amount_total': 0.0, + } + val = val1 = 0.0 + order_list = self.sales_order_lines + #_logger.info("order %s " %order.id) + val_tax = 0.0 + val_untax = 0.0 + if order_list: + cur = self.pricelist_id.currency_id + for line in order.sales_order_lines: + if line.state in ['done','approved']: + val += line.amount_total + #val_tax += val.get('amount_tax',0.0) + #val_untax += val.get('amount_untaxed',0.0) + + res['amount_tax'] = cur.round(val_tax) + res['amount_untaxed'] = val + res['amount_total']= val + + #_logger.info("Res : %s " %res) + + order.write({'amount_untaxed': res['amount_untaxed']}) + order.write({'amount_tax': res['amount_tax']}) + order.write({'amount_total': res['amount_total']}) + _logger.info("res : %s" %(res)) + _logger.warning("returning : %s" %res) + return res + + + @api.multi + def subscribe(self, partner): + self.message_subscribe(partner_ids=[(partner.id)]) + self.message_post(body=("Order line created by %s" %(partner.name))) + + @api.multi + def button_details(self): + context = self.env.context.copy() + view_id = self.env.ref( + 'sale.' + 'view_order_form').id + + context['default_is_cp'] = True + context['default_cp_order_id'] = self.id + context['supplier'] = self.partner_id + #context['default_partner_id'] = self.partner_id.id + context['default_pricelist_id'] = self.pricelist_id.id + context['default_order_date'] = self.date_order + context['default_date_planned'] = self.deadline_date + context['view_buttons'] = True + #context['fiscal_position'] = self.fiscal_position + #context['state'] = self.state + #context['parent'] = self.id + + #partial_id = self.pool.get("purchase.collective.order.line").create(context=context) + + view = { + 'name': _('Details'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'sale.order', + 'view_id': view_id, + 'type': 'ir.actions.act_window', + 'target': 'new', + 'readonly': False, + #'res_id': partial_id, + 'context': context + } + return view + + def _get_order(self, ids): + result = {} + for line in self.pool.get('sale.order').browse(ids): + result[line.id] = True + return result.keys() + """ + def create(self, vals=None): + _logger.warning("Creating order %s" %(vals)) + + if vals.get('name','/')=='/': + vals['name'] = self.env['ir.sequence'].get('purchase_collective.order', context=context) or '/' + if not vals.get('location_id'): + for user_obj in self.env['res.users'].browse(uid, context=context): + vals['location_id'] = user_obj.partner_id.property_stock_supplier.id + #logging.info('adding location info %s' %vals.get('location_id')) + if not vals.get('pricelist_id'): + for user_obj in self.env['res.users'].browse(uid, context=context): + vals['pricelist_id'] = user_obj.partner_id.property_product_pricelist.id + #logging.info('adding pricelist %s' %vals.get('pricelist_id')) + order = super(PurchaseCollectiveOrder, self).create(vals) + return order + """ + + def unlink(self, ids): + purchase_orders = self.read(ids, ['state'], context=context) + unlink_ids = [] + for s in purchase_orders: + if s['state'] in ['draft','cancel']: + unlink_ids.append(s['id']) + else: + raise Warning(_('Invalid Action!'), _('In order to delete a purchase order, you must cancel it first.')) + + # automatically sending subflow.delete upon deletion + self.signal_workflow(unlink_ids, 'purchase_cancel') + + return super(PurchaseCollectiveOrder, self).unlink(unlink_ids, context=context) + + def copy(self, id, default=None, context=None): + new_id = super(PurchaseCollectiveOrder, self).copy(id, default=default, context=context) + for po in self.browse([new_id], context=context): + for line in po.order_line: + vals = self.env['purchase_collective.order_line'].onchange_product_id( + line.id, po.pricelist_id.id, line.product_id.id, line.product_qty, + line.product_uom.id, po.partner_id.id, date_order=po.date_order, context=context + ) + if vals.get('value', {}).get('date_planned'): + line.write({'date_planned': vals['value']['date_planned']}) + return new_id + + def set_order_line_status(self, ids, status, context=None): + """ + line = self.pool.get('purchase_collective.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) + if order_line_ids and status == 'cancel': + procs = proc_obj.search(cr, uid, [('purchase_line_id', 'in', order_line_ids)], context=context) + if procs: + proc_obj.write(cr, uid, procs, {'state': 'exception'}, context=context) + """ + return True + + def wkf_confirm_order(self, ids, context=None): + #_logger.info("Confirmando orden : %s" %ids) + todo = [] + for po in self.browse(ids, context=context): + if not any(line.state != 'cancel' for line in po.order_line): + raise Warning(_('Error!'),_('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_collective.order_line').action_confirm(cr, uid, todo, context) + for id in ids: + self.write(id, {'state' : 'confirmed', 'validator' : uid}, context=context) + return True + + def _set_po_lines_invoiced(self, 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') + and 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) + elif po_line.product_id.type == 'service': + 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_collective.order_line'].write(cr, uid, is_invoiced, {'invoiced': True}) + workflow.trg_write(uid, 'purchase_colective.order', po.id, cr) + """ + + def action_picking_create(self, ids, context=None): + for order in self.browse(ids): + picking_vals = { + 'picking_type_id': order.picking_type_id.id, + 'partner_id': order.partner_id.id, + 'date': order.date_order, + 'origin': order.name + } + picking_id = self.env['stock.picking'].create(picking_vals, context=context) + # esta llamada da fallo + #self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context) + return picking_id + + # get the picking type for the warehouse defined in order + def _get_picking_in(self, context=None): + type_obj = self.env['stock.picking.type'] + types = type_obj.search([('code', '=', 'incoming'),('warehouse_id.id','=',self.wh_id.id)], context=context) + if not types: + types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context) + if not types: + raise Warning(_('Error!'), _("Make sure you have defined the warehouse in the order!")) + #logging.info("Types %s" %types) + return types[0] + + _defaults = { + 'date_order': fields.datetime.now, + 'state': 'draft', + 'name': '/', + 'shipped': 0, + 'invoice_method': 'order', + 'invoiced': 0, + 'picking_type_id': _get_picking_in, + } + + + + + diff --git a/purchase_collective/models/sale_order.py b/purchase_collective/models/sale_order.py new file mode 100644 index 0000000..3a86f21 --- /dev/null +++ b/purchase_collective/models/sale_order.py @@ -0,0 +1,16 @@ +import random + +from odoo import models, api, fields + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + cp_order_id = fields.Many2one( + 'purchase_collective.order', + string='Parent Collective Purchase', + help='The collective purchase wich this order belongs', + ondelete='restrict' + ) + is_cp = fields.Boolean(string="Is part of a Collective Purchase") + diff --git a/purchase_collective/security/ir.model.access.csv b/purchase_collective/security/ir.model.access.csv new file mode 100644 index 0000000..417694f --- /dev/null +++ b/purchase_collective/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_collective_order_public,purchase_collective.order,model_purchase_collective_order,base.group_public,1,0,0,0 +access_purchase_collective_order,purchase_collective.order,model_purchase_collective_order,group_purchase_collective_user,1,0,1,0 +access_purchase_collective_order_manager,purchase_collective.order,model_purchase_collective_order,group_purchase_collective_manager,1,1,1,1 diff --git a/purchase_collective/security/purchase_collective_security.xml b/purchase_collective/security/purchase_collective_security.xml new file mode 100644 index 0000000..ecd966e --- /dev/null +++ b/purchase_collective/security/purchase_collective_security.xml @@ -0,0 +1,48 @@ + + + + + + Collective Purchase + Make bulk purchases from several users + 20 + + + + User + + + + + + Manager + + + + + + + Collective Purchase Order All Read + + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.parent_id.id])] + + + + + + + + Collective Purchase Order multi-company + + + ['|',('create_uid','=',False),('create_uid','=',user.id)] + + + + + + + + diff --git a/purchase_collective/views/actions.xml b/purchase_collective/views/actions.xml new file mode 100644 index 0000000..41b7da3 --- /dev/null +++ b/purchase_collective/views/actions.xml @@ -0,0 +1,56 @@ + + + + + Open + ir.actions.act_window + purchase_collective.order + form + tree,form + { + 'search_default_my_sale_orders_filter': 1 + } + + + + Click to create a new Collective Purchase. + + + + + + Closed + ir.actions.act_window + purchase_collective.order + form + tree,form + { + 'search_default_my_sale_orders_filter': 1 + } + + + + Click to create a new Collective Purchase. + + + + + + Cancelled + ir.actions.act_window + purchase_collective.order + form + tree,form + { + 'search_default_my_sale_orders_filter': 1 + } + + + + Click to create a new Collective Purchase. + + + + + + diff --git a/purchase_collective/views/menus.xml b/purchase_collective/views/menus.xml new file mode 100644 index 0000000..f15e215 --- /dev/null +++ b/purchase_collective/views/menus.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/purchase_collective/views/purchase_collective.xml b/purchase_collective/views/purchase_collective.xml new file mode 100644 index 0000000..dc83760 --- /dev/null +++ b/purchase_collective/views/purchase_collective.xml @@ -0,0 +1,111 @@ + + + + + purchase.collective.order.form.view + purchase_collective.order + + + + + + + + + + + + + + + + + + + + + + + + + + + + Request for Quotation + Purchase Order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/purchase_collective/views/sale_order.xml b/purchase_collective/views/sale_order.xml new file mode 100644 index 0000000..a5a0b1c --- /dev/null +++ b/purchase_collective/views/sale_order.xml @@ -0,0 +1,18 @@ + + + + + sale order collective purchase extension + sale.order + + + + + + + + + + + +
+ Click to create a new Collective Purchase. +