Pedido - Reserva de produtos com Atributos


#1

Pessoal, estou com um problema na Ordem de Venda que preciso de ajuda para solucionar, não consegui encontrar um caminho que não tenha que alterar a forma como o sistema efetua reservas na M_Storage a partir de um pedido de vendas, pesquisei nos foruns e também não encontrei nada relevante.

Uso a branch kenos que esta no Mercurial + 360 lts com algumas customizações internas para adaptar a nossa realidade.

O problema ocorre quando preciso alterar a linha de um pedido que já efetuou a reserva de produtos na M_Storage, geralmente pedidos com status Em Progresso ou Re-Ativados, se altero a M_AttributeSetInstance_ID na C_OrderLine (nós usamos Lote e Validade para rastreabilidade de produtos), a mudança não reflete na M_Storage e quando completo a Remessa, é gerado uma qtyReserved negativa. Por Exemplo:

  1. Completo um pedido de venda com 5 unidades do produto “A” e Instancia de Atributo “A1” (O produto possui dois lotes)
  2. Minha M_Storage fica com dois registros para o produto:
    M_Product_ID | M_AttributeSetInstance_ID| qtyOnHand | qtyReserved | qtyOrdered | M_Locator_ID
    2000100 | 2000555 | 70 | 5 | 0 | 2000000
    2000100 | 2000556 | 36 | 0 | 0 | 2000000
  3. ReAtivo meu pedido e altero a Instancia de Atributo na linha do produto “A”, mudo para “A2” e completo o pedido.
  4. Ao completar a Remessa/Expedição a M_Storage fica assim:
    M_Product_ID | M_AttributeSetInstance_ID| qtyOnHand | qtyReserved | qtyOrdered | M_Locator_ID
    2000100 | 2000555 | 70 | 5 | 0 | 2000000
    2000100 | 2000556 | 31 | -5| 0 | 2000000
  5. A baixa é feita na instancia correta mas a reserva continua na instancia anterior

Analisei a customização da MOrder, MOrderLine, criar um callOut mas sempre encontro contras e excessões. Alguém já passou por este problema ou tem alguma idéia de como posso resolver esta questão?

Obrigado

Kaio Yoshida


#2

Kaio Yoshida,

se você completou a venda na primeira situação com o produto A instância A1, você deve estornar a nota fiscal eletrônica, fatura, entrega e reativar o pedido, depois você altera a linha do pedido colocando a nova instância A2 e completar a venda.

Verifique se dá certo, pois acho que seja assim.

Espero ter ajudado.

Abraço.


#3

Bom dia Paulo, o problema ocorre justamente nesta situação, ao reativar o pedido e alterar a Instância de atributo para A2, o sistema não retira a reserva anterior (A1) e não inclui a reserve nova (A2).

Faz sentido porque na MOrder.reserveStock temos o seguinte código:

		//	Binding
		BigDecimal target = binding ? line.getQtyOrdered() : Env.ZERO;
		BigDecimal difference = target
			.subtract(line.getQtyReserved())
			.subtract(line.getQtyDelivered());
		if (difference.signum() == 0)
		{
			MProduct product = line.getProduct();
			if (product != null)
			{
				Volume = Volume.add(product.getVolume().multiply(line.getQtyOrdered()));
				Weight = Weight.add(product.getWeight().multiply(line.getQtyOrdered()));
			}
			continue;
		}

Onde o Adempiere trata sómente alterações de quantidade.

Acredito que o questão principal é comparar a M_AttributeSetInstance_ID Source (anterior) e Target (nova) e caso tenha diferença e a Source for maior que zero, remover a reserva da source e aplicar a nova reserva na target basado na QtyOrdered do documento.

Mas gostaria de saber se mais alguém passou por isso e conseguiu resolver de outra forma ou alguém que conheça melhor o sistema e o Java possa me indicar um caminho mais fácil.

Obrigado

Kaio


#4

Kaio,

o problema pode ser este mesmo, pois ele não leva em consideração o m_attributesetinstance_id.

Creio que isto ainda não foi resolvido no código, mas para desencargo de consciência, tente pegar a branch da kenos, LBR39 no mercurial e verifique se eles alteraram essa situação.

Abraço.


#5

Paulo,

A branch LBR-39 tem as modificações para transmissão de CC-e e não corrige este erro.

Kaio,

Sugiro abrir um ticket em: adempiere.atlassian.net/secure/Dashboard.jspa que eu tento resolver durante a próxima semana se sobrar algum tempo.

Abs!


#6

Criei o ticket “ADEMPIERE-124” na ferramenta, do tipo Bug e prioridade Major, esta na pendência da triagem para ser encaminhado.

Valeu Paulo e Ricardo, obrigado pela ajuda, se eu tiver sucesso aqui eu posto o código para validação.

Abçs

Kaio


#7

O problema não é só isso… a M_Storage é muito “bugada” com relação a QtyReserved e QtyOrdered, isso pq na C_OrderLine vc não atribui um localizador para a sua reserva, ou seja, vc coloca um pedido de uma camiseta do armazém loja 1… porém o localizador padrão da loja 1 é patreleira 2… Se vc trocar o localizador padrão para patreleira 3 na cadastro dos localizadores, e dai alterar o seu pedido, já bagunçou toda a sua M_Storage e suas quantidades reservadas…

Existem vários posts no fórum do adempiere sobre a M_Storage, mas parece que até hoje ninguém realmente analisou todos os problemas e corrigiu


#8

Me parece que o sistema não lida muito bem com estoque de produtos com atributos, como lido com produtos com Validade e Lote eu precisei fazer algumas customizações para lidar com o M_Locator_ID.

Inclusive neste caso da MOrder.reserveStock o sistema busca o M_Locator_ID na M_Storage e se não encontra, pega o default do produto, o que ao meu ver pode gerar problemas, no meu caso específico, eu precisei customizar o atributo como obrigatório na linha do pedido e criar um callout que busca automaticamente na M_Storage o Lote desponível com menor data de validade ao entrar com o produto, este callOut também insere na C_OrderLine o M_Locator_ID referente ao M_AttributeSetInstance_ID no inventário.


#9

Consegui resolver o problema e parece que esta OK, preciso completar mais alguns testcases mas a principio funcionou, ficou bem específico, não sei se serve para todos os casos.

A implementação ficou apenas no MOrder.reserveStock e eu inclui uma nova coluna M_AttributeSetInstanceTo_ID (integer, default 0), com os métodos get e set no Model do PO, na C_OrderLine para comparar com a M_AttributeSetInstance_ID, se os valores forem validos e distintos na comparação das duas colunas, eu chamo o MStorage.add para os dois Atributos de Instância. Segue o código completo:

	private boolean reserveStock (MDocType dt, MOrderLine[] lines)
	{
		if (dt == null)
			dt = MDocType.get(getCtx(), getC_DocType_ID());

		//	Binding
		boolean binding = !dt.isProposal();
		//	Not binding - i.e. Target=0
		if (DOCACTION_Void.equals(getDocAction())
			//	Closing Binding Quotation
			|| (MDocType.DOCSUBTYPESO_Quotation.equals(dt.getDocSubTypeSO())
				&& DOCACTION_Close.equals(getDocAction()))
			) // || isDropShip() )
			binding = false;
		boolean isSOTrx = isSOTrx();
		log.fine("Binding=" + binding + " - IsSOTrx=" + isSOTrx);
		//	Force same WH for all but SO/PO
		int header_M_Warehouse_ID = getM_Warehouse_ID();
		if (MDocType.DOCSUBTYPESO_StandardOrder.equals(dt.getDocSubTypeSO())
			|| MDocType.DOCBASETYPE_PurchaseOrder.equals(dt.getDocBaseType()))
			header_M_Warehouse_ID = 0;		//	don't enforce

		BigDecimal Volume = Env.ZERO;
		BigDecimal Weight = Env.ZERO;

		//	Always check and (un) Reserve Inventory
		for (int i = 0; i < lines.length; i++)
		{
			MOrderLine line = lines[i];
			//	Check/set WH/Org
			if (header_M_Warehouse_ID != 0)	//	enforce WH
			{
				if (header_M_Warehouse_ID != line.getM_Warehouse_ID())
					line.setM_Warehouse_ID(header_M_Warehouse_ID);
				if (getAD_Org_ID() != line.getAD_Org_ID())
					line.setAD_Org_ID(getAD_Org_ID());
			}
			//	Binding
			BigDecimal target = binding ? line.getQtyOrdered() : Env.ZERO;
			BigDecimal difference = target
				.subtract(line.getQtyReserved())
				.subtract(line.getQtyDelivered());
			if (difference.signum() == 0 && !chkAttribute(line)) // add chkAttribute
			{
				MProduct product = line.getProduct();
				if (product != null)
				{
					Volume = Volume.add(product.getVolume().multiply(line.getQtyOrdered()));
					Weight = Weight.add(product.getWeight().multiply(line.getQtyOrdered()));
				}
				continue;
			}

			log.fine("Line=" + line.getLine()
				+ " - Target=" + target + ",Difference=" + difference
				+ " - Ordered=" + line.getQtyOrdered()
				+ ",Reserved=" + line.getQtyReserved() + ",Delivered=" + line.getQtyDelivered());

			//	Check Product - Stocked and Item
			MProduct product = line.getProduct();
			if (product != null)
			{
				if (product.isStocked())
				{
					BigDecimal ordered = isSOTrx ? Env.ZERO : difference;
					BigDecimal reserved = isSOTrx ? difference : Env.ZERO;
					int M_Locator_ID = getLocator(line, product);
					//	Update Storage
					if (!updStorage(line, reserved, ordered, M_Locator_ID, line.getM_AttributeSetInstance_ID()))
						return false;
				}	//	stockec
				
				// Release reserved if have an old Attribute 
				// and update for the new one
				if (chkAttribute(line))
				{
					BigDecimal reserved = line.getQtyReserved();
					int M_Locator_ID = getLocator(line, product);
					//	Update Storage Old
					if (!updStorage(line, reserved.negate(), Env.ZERO, M_Locator_ID, line.getM_AttributeSetInstanceTo_ID()))
						return false;
					//	Update Storage New
					if (!updStorage(line, reserved, Env.ZERO, M_Locator_ID, line.getM_AttributeSetInstance_ID()))
						return false;
					//	Save the value of M_AttributeSetInstance_ID into M_AttributeSetInstanceTo_ID
					if (!updAttribute(line))
						return false;
				}
				
				//	update line
				line.setQtyReserved(line.getQtyReserved().add(difference));
				if (!line.save(get_TrxName()))
					return false;
				//
				Volume = Volume.add(product.getVolume().multiply(line.getQtyOrdered()));
				Weight = Weight.add(product.getWeight().multiply(line.getQtyOrdered()));
			}	//	product
		}	//	reverse inventory

		setVolume(Volume);
		setWeight(Weight);
		return true;
	}	//	reserveStock