mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 18:59:08 +00:00
Merge pull request #39880 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -56,7 +56,9 @@
|
|||||||
"Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {},
|
"Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {},
|
||||||
"Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {},
|
"Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {},
|
||||||
"Im\u00f3veis Destinados \u00e0 Venda": {},
|
"Im\u00f3veis Destinados \u00e0 Venda": {},
|
||||||
"Insumos (materiais diretos)": {},
|
"Insumos (materiais diretos)": {
|
||||||
|
"account_type": "Stock"
|
||||||
|
},
|
||||||
"Insumos Agropecu\u00e1rios": {},
|
"Insumos Agropecu\u00e1rios": {},
|
||||||
"Mercadorias para Revenda": {},
|
"Mercadorias para Revenda": {},
|
||||||
"Outras 11": {},
|
"Outras 11": {},
|
||||||
@@ -146,6 +148,65 @@
|
|||||||
"root_type": "Asset"
|
"root_type": "Asset"
|
||||||
},
|
},
|
||||||
"CUSTOS DE PRODU\u00c7\u00c3O": {
|
"CUSTOS DE PRODU\u00c7\u00c3O": {
|
||||||
|
"CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": {
|
||||||
|
"CUSTO DOS PRODUTOS VENDIDOS": {
|
||||||
|
"CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": {
|
||||||
|
"Custos dos Produtos Vendidos em Geral": {
|
||||||
|
"account_type": "Cost of Goods Sold"
|
||||||
|
},
|
||||||
|
"Outros Custos 4": {},
|
||||||
|
"account_type": "Cost of Goods Sold"
|
||||||
|
},
|
||||||
|
"CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": {
|
||||||
|
"Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {},
|
||||||
|
"Custos dos Produtos para Assist\u00eancia Social - Vendidos": {},
|
||||||
|
"Outras": {}
|
||||||
|
},
|
||||||
|
"CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": {
|
||||||
|
"Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {},
|
||||||
|
"Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {},
|
||||||
|
"Outros Custos 6": {}
|
||||||
|
},
|
||||||
|
"CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": {
|
||||||
|
"Custos dos Produtos para Sa\u00fade - Gratuidades": {},
|
||||||
|
"Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {},
|
||||||
|
"Outros Custos 5": {}
|
||||||
|
},
|
||||||
|
"account_type": "Cost of Goods Sold"
|
||||||
|
},
|
||||||
|
"CUSTO DOS SERVI\u00c7OS PRESTADOS": {
|
||||||
|
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": {
|
||||||
|
"Custo dos Servi\u00e7os Prestados em Geral": {},
|
||||||
|
"Outros Custos": {}
|
||||||
|
},
|
||||||
|
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": {
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Gratuidade 1": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {},
|
||||||
|
"Outros Custos 2": {}
|
||||||
|
},
|
||||||
|
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": {
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Gratuidade": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados ao PROUNI": {},
|
||||||
|
"Outros Custos 1": {}
|
||||||
|
},
|
||||||
|
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": {
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Gratuidade 2": {},
|
||||||
|
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {},
|
||||||
|
"Outros Custos 3": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CUSTO DOS BENS E SERVI\u00c7OS PRODUZIDOS": {
|
"CUSTO DOS BENS E SERVI\u00c7OS PRODUZIDOS": {
|
||||||
"CUSTO DOS PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": {
|
"CUSTO DOS PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": {
|
||||||
"Alimenta\u00e7\u00e3o do Trabalhador": {},
|
"Alimenta\u00e7\u00e3o do Trabalhador": {},
|
||||||
@@ -621,7 +682,9 @@
|
|||||||
"Receita das Unidades Imobili\u00e1rias Vendidas": {},
|
"Receita das Unidades Imobili\u00e1rias Vendidas": {},
|
||||||
"Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {},
|
"Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {},
|
||||||
"Receita de Exporta\u00e7\u00e3o de Servi\u00e7os": {},
|
"Receita de Exporta\u00e7\u00e3o de Servi\u00e7os": {},
|
||||||
"Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {},
|
"Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
},
|
||||||
"Receita de Vendas de Mercadorias e Produtos a Comercial Exportadora com Fim Espec\u00edfico de Exporta\u00e7\u00e3o": {}
|
"Receita de Vendas de Mercadorias e Produtos a Comercial Exportadora com Fim Espec\u00edfico de Exporta\u00e7\u00e3o": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,65 +708,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RESULTADO OPERACIONAL": {
|
"RESULTADO OPERACIONAL": {
|
||||||
"CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": {
|
|
||||||
"CUSTO DOS PRODUTOS VENDIDOS": {
|
|
||||||
"CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": {
|
|
||||||
"Custos dos Produtos Vendidos em Geral": {
|
|
||||||
"account_type": "Cost of Goods Sold"
|
|
||||||
},
|
|
||||||
"Outros Custos 4": {},
|
|
||||||
"account_type": "Cost of Goods Sold"
|
|
||||||
},
|
|
||||||
"CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": {
|
|
||||||
"Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {},
|
|
||||||
"Custos dos Produtos para Assist\u00eancia Social - Vendidos": {},
|
|
||||||
"Outras": {}
|
|
||||||
},
|
|
||||||
"CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": {
|
|
||||||
"Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {},
|
|
||||||
"Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {},
|
|
||||||
"Outros Custos 6": {}
|
|
||||||
},
|
|
||||||
"CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": {
|
|
||||||
"Custos dos Produtos para Sa\u00fade - Gratuidades": {},
|
|
||||||
"Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {},
|
|
||||||
"Outros Custos 5": {}
|
|
||||||
},
|
|
||||||
"account_type": "Cost of Goods Sold"
|
|
||||||
},
|
|
||||||
"CUSTO DOS SERVI\u00c7OS PRESTADOS": {
|
|
||||||
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": {
|
|
||||||
"Custo dos Servi\u00e7os Prestados em Geral": {},
|
|
||||||
"Outros Custos": {}
|
|
||||||
},
|
|
||||||
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": {
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Gratuidade 1": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {},
|
|
||||||
"Outros Custos 2": {}
|
|
||||||
},
|
|
||||||
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": {
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Gratuidade": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados ao PROUNI": {},
|
|
||||||
"Outros Custos 1": {}
|
|
||||||
},
|
|
||||||
"CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": {
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Gratuidade 2": {},
|
|
||||||
"Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {},
|
|
||||||
"Outros Custos 3": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DESPESAS OPERACIONAIS": {
|
"DESPESAS OPERACIONAIS": {
|
||||||
"DESPESAS OPERACIONAIS 1": {
|
"DESPESAS OPERACIONAIS 1": {
|
||||||
"DESPESAS OPERACIONAIS 2": {
|
"DESPESAS OPERACIONAIS 2": {
|
||||||
|
|||||||
@@ -2651,7 +2651,7 @@ def get_advance_journal_entries(
|
|||||||
|
|
||||||
if order_list:
|
if order_list:
|
||||||
q = q.where(
|
q = q.where(
|
||||||
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_type).isin(order_list))
|
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
|
||||||
)
|
)
|
||||||
|
|
||||||
q = q.orderby(journal_entry.posting_date)
|
q = q.orderby(journal_entry.posting_date)
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ class BuyingController(SubcontractingController):
|
|||||||
lc_voucher_data = frappe.db.sql(
|
lc_voucher_data = frappe.db.sql(
|
||||||
"""select sum(applicable_charges), cost_center
|
"""select sum(applicable_charges), cost_center
|
||||||
from `tabLanded Cost Item`
|
from `tabLanded Cost Item`
|
||||||
where docstatus = 1 and purchase_receipt_item = %s""",
|
where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""",
|
||||||
d.name,
|
(d.name, self.name),
|
||||||
)
|
)
|
||||||
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
|
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
|
||||||
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
|
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError):
|
|||||||
class StockController(AccountsController):
|
class StockController(AccountsController):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
super(StockController, self).validate()
|
super(StockController, self).validate()
|
||||||
|
|
||||||
|
if self.docstatus == 0:
|
||||||
|
self.validate_duplicate_serial_and_batch_bundle()
|
||||||
if not self.get("is_return"):
|
if not self.get("is_return"):
|
||||||
self.validate_inspection()
|
self.validate_inspection()
|
||||||
self.validate_serialized_batch()
|
self.validate_serialized_batch()
|
||||||
@@ -55,6 +58,32 @@ class StockController(AccountsController):
|
|||||||
self.validate_internal_transfer()
|
self.validate_internal_transfer()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
|
|
||||||
|
def validate_duplicate_serial_and_batch_bundle(self):
|
||||||
|
if sbb_list := [
|
||||||
|
item.get("serial_and_batch_bundle")
|
||||||
|
for item in self.items
|
||||||
|
if item.get("serial_and_batch_bundle")
|
||||||
|
]:
|
||||||
|
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
data = (
|
||||||
|
frappe.qb.from_(SLE)
|
||||||
|
.select(SLE.voucher_type, SLE.voucher_no, SLE.serial_and_batch_bundle)
|
||||||
|
.where(
|
||||||
|
(SLE.docstatus == 1)
|
||||||
|
& (SLE.serial_and_batch_bundle.notnull())
|
||||||
|
& (SLE.serial_and_batch_bundle.isin(sbb_list))
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
data = data[0]
|
||||||
|
frappe.throw(
|
||||||
|
_("Serial and Batch Bundle {0} is already used in {1} {2}.").format(
|
||||||
|
frappe.bold(data.serial_and_batch_bundle), data.voucher_type, data.voucher_no
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
@@ -156,14 +185,18 @@ class StockController(AccountsController):
|
|||||||
if self.doctype == "Stock Reconciliation":
|
if self.doctype == "Stock Reconciliation":
|
||||||
qty = row.qty
|
qty = row.qty
|
||||||
type_of_transaction = "Inward"
|
type_of_transaction = "Inward"
|
||||||
|
warehouse = row.warehouse
|
||||||
else:
|
else:
|
||||||
qty = row.stock_qty
|
qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty
|
||||||
type_of_transaction = get_type_of_transaction(self, row)
|
type_of_transaction = get_type_of_transaction(self, row)
|
||||||
|
warehouse = (
|
||||||
|
row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse
|
||||||
|
)
|
||||||
|
|
||||||
sn_doc = SerialBatchCreation(
|
sn_doc = SerialBatchCreation(
|
||||||
{
|
{
|
||||||
"item_code": row.item_code,
|
"item_code": row.item_code,
|
||||||
"warehouse": row.warehouse,
|
"warehouse": warehouse,
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
@@ -938,6 +971,9 @@ class StockController(AccountsController):
|
|||||||
"Stock Reconciliation",
|
"Stock Reconciliation",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not frappe.get_all("Putaway Rule", limit=1):
|
||||||
|
return
|
||||||
|
|
||||||
if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
|
if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
|
||||||
valid_doctype = False
|
valid_doctype = False
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item Code",
|
"label": "Item Code",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item_name",
|
"fieldname": "item_name",
|
||||||
@@ -53,7 +54,8 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "For Warehouse",
|
"label": "For Warehouse",
|
||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 1,
|
"columns": 1,
|
||||||
@@ -141,7 +143,8 @@
|
|||||||
"fieldname": "from_warehouse",
|
"fieldname": "from_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "From Warehouse",
|
"label": "From Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "item_code.safety_stock",
|
"fetch_from": "item_code.safety_stock",
|
||||||
@@ -199,7 +202,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-12 12:09:08.358326",
|
"modified": "2024-02-11 16:21:11.977018",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Material Request Plan Item",
|
"name": "Material Request Plan Item",
|
||||||
|
|||||||
@@ -298,7 +298,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested",
|
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
@@ -436,7 +437,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-26 16:31:13.740777",
|
"modified": "2024-02-11 15:42:47.642481",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan",
|
"name": "Production Plan",
|
||||||
|
|||||||
@@ -312,9 +312,10 @@ class ProductionPlan(Document):
|
|||||||
so_item.parent,
|
so_item.parent,
|
||||||
so_item.item_code,
|
so_item.item_code,
|
||||||
so_item.warehouse,
|
so_item.warehouse,
|
||||||
(
|
so_item.qty,
|
||||||
(so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
|
so_item.work_order_qty,
|
||||||
).as_("pending_qty"),
|
so_item.delivered_qty,
|
||||||
|
so_item.conversion_factor,
|
||||||
so_item.description,
|
so_item.description,
|
||||||
so_item.name,
|
so_item.name,
|
||||||
so_item.bom_no,
|
so_item.bom_no,
|
||||||
@@ -337,6 +338,11 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
items = items_query.run(as_dict=True)
|
items = items_query.run(as_dict=True)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item.pending_qty = (
|
||||||
|
flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor
|
||||||
|
)
|
||||||
|
|
||||||
pi = frappe.qb.DocType("Packed Item")
|
pi = frappe.qb.DocType("Packed Item")
|
||||||
|
|
||||||
packed_items_query = (
|
packed_items_query = (
|
||||||
@@ -646,7 +652,10 @@ class ProductionPlan(Document):
|
|||||||
"project": self.project,
|
"project": self.project,
|
||||||
}
|
}
|
||||||
|
|
||||||
key = (d.item_code, d.sales_order, d.warehouse)
|
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse)
|
||||||
|
if self.combine_items:
|
||||||
|
key = (d.item_code, d.sales_order, d.warehouse)
|
||||||
|
|
||||||
if not d.sales_order:
|
if not d.sales_order:
|
||||||
key = (d.name, d.item_code, d.warehouse)
|
key = (d.name, d.item_code, d.warehouse)
|
||||||
|
|
||||||
@@ -1762,23 +1771,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
|||||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.request_cache
|
||||||
def get_non_completed_production_plans():
|
def get_non_completed_production_plans():
|
||||||
table = frappe.qb.DocType("Production Plan")
|
table = frappe.qb.DocType("Production Plan")
|
||||||
child = frappe.qb.DocType("Production Plan Item")
|
child = frappe.qb.DocType("Production Plan Item")
|
||||||
|
|
||||||
query = (
|
return (
|
||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
.inner_join(child)
|
.inner_join(child)
|
||||||
.on(table.name == child.parent)
|
.on(table.name == child.parent)
|
||||||
.select(table.name)
|
.select(table.name)
|
||||||
|
.distinct()
|
||||||
.where(
|
.where(
|
||||||
(table.docstatus == 1)
|
(table.docstatus == 1)
|
||||||
& (table.status.notin(["Completed", "Closed"]))
|
& (table.status.notin(["Completed", "Closed"]))
|
||||||
& (child.planned_qty > child.ordered_qty)
|
& (child.planned_qty > child.ordered_qty)
|
||||||
)
|
)
|
||||||
).run(as_dict=True)
|
).run(pluck="name")
|
||||||
|
|
||||||
return list(set([d.name for d in query]))
|
|
||||||
|
|
||||||
|
|
||||||
def get_raw_materials_of_sub_assembly_items(
|
def get_raw_materials_of_sub_assembly_items(
|
||||||
|
|||||||
@@ -447,7 +447,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Production Plan",
|
"options": "Production Plan",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "production_plan_item",
|
"fieldname": "production_plan_item",
|
||||||
@@ -592,7 +593,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-11 18:35:49.852069",
|
"modified": "2024-02-11 15:47:13.454422",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order",
|
"name": "Work Order",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item Code",
|
"label": "Item Code",
|
||||||
"options": "Item"
|
"options": "Item",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "source_warehouse",
|
"fieldname": "source_warehouse",
|
||||||
@@ -141,7 +142,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-09-28 10:50:43.512562",
|
"modified": "2024-02-11 15:45:32.318374",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Item",
|
"name": "Work Order Item",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_
|
|||||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||||
WarehouseRequired,
|
WarehouseRequired,
|
||||||
|
create_pick_list,
|
||||||
make_delivery_note,
|
make_delivery_note,
|
||||||
make_material_request,
|
make_material_request,
|
||||||
make_raw_material_request,
|
make_raw_material_request,
|
||||||
@@ -1973,6 +1974,83 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
||||||
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
||||||
|
|
||||||
|
def test_pick_list_without_rejected_materials(self):
|
||||||
|
serial_and_batch_item = make_item(
|
||||||
|
"_Test Serial and Batch Item for Rejected Materials",
|
||||||
|
properties={
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BAT-TSBIFRM-.#####",
|
||||||
|
"serial_no_series": "SN-TSBIFRM-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
serial_item = make_item(
|
||||||
|
"_Test Serial Item for Rejected Materials",
|
||||||
|
properties={
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "SN-TSIFRM-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
batch_item = make_item(
|
||||||
|
"_Test Batch Item for Rejected Materials",
|
||||||
|
properties={
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BAT-TBIFRM-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
normal_item = make_item("_Test Normal Item for Rejected Materials").name
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC"
|
||||||
|
|
||||||
|
if not frappe.db.exists("Warehouse", rejected_warehouse):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Warehouse",
|
||||||
|
"warehouse_name": rejected_warehouse,
|
||||||
|
"company": "_Test Company",
|
||||||
|
"warehouse_group": "_Test Warehouse Group",
|
||||||
|
"is_rejected_warehouse": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True)
|
||||||
|
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||||
|
se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse})
|
||||||
|
|
||||||
|
se.save()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True
|
||||||
|
)
|
||||||
|
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||||
|
se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse})
|
||||||
|
|
||||||
|
se.save()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True)
|
||||||
|
|
||||||
|
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||||
|
so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse})
|
||||||
|
|
||||||
|
so.save()
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
pick_list = create_pick_list(so.name)
|
||||||
|
|
||||||
|
pick_list.save()
|
||||||
|
for row in pick_list.locations:
|
||||||
|
self.assertEqual(row.qty, 1.0)
|
||||||
|
self.assertFalse(row.warehouse == rejected_warehouse)
|
||||||
|
self.assertTrue(row.warehouse == warehouse)
|
||||||
|
|
||||||
|
|
||||||
def automatically_fetch_payment_terms(enable=1):
|
def automatically_fetch_payment_terms(enable=1):
|
||||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||||
|
|||||||
@@ -1122,6 +1122,7 @@ def validate_cancelled_item(item_code, docstatus=None):
|
|||||||
frappe.throw(_("Item {0} is cancelled").format(item_code))
|
frappe.throw(_("Item {0} is cancelled").format(item_code))
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.request_cache
|
||||||
def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
||||||
"""returns last purchase details in stock uom"""
|
"""returns last purchase details in stock uom"""
|
||||||
# get last purchase order item details
|
# get last purchase order item details
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class LandedCostVoucher(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.check_mandatory()
|
self.check_mandatory()
|
||||||
self.validate_receipt_documents()
|
self.validate_receipt_documents()
|
||||||
|
self.validate_line_items()
|
||||||
init_landed_taxes_and_totals(self)
|
init_landed_taxes_and_totals(self)
|
||||||
self.set_total_taxes_and_charges()
|
self.set_total_taxes_and_charges()
|
||||||
if not self.get("items"):
|
if not self.get("items"):
|
||||||
@@ -72,6 +73,26 @@ class LandedCostVoucher(Document):
|
|||||||
|
|
||||||
self.set_applicable_charges_on_item()
|
self.set_applicable_charges_on_item()
|
||||||
|
|
||||||
|
def validate_line_items(self):
|
||||||
|
for d in self.get("items"):
|
||||||
|
if (
|
||||||
|
d.docstatus == 0
|
||||||
|
and d.purchase_receipt_item
|
||||||
|
and not frappe.db.exists(
|
||||||
|
d.receipt_document_type + " Item",
|
||||||
|
{"name": d.purchase_receipt_item, "parent": d.receipt_document},
|
||||||
|
)
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row {0}: {2} Item {1} does not exist in {2} {3}").format(
|
||||||
|
d.idx,
|
||||||
|
frappe.bold(d.purchase_receipt_item),
|
||||||
|
d.receipt_document_type,
|
||||||
|
frappe.bold(d.receipt_document),
|
||||||
|
),
|
||||||
|
title=_("Incorrect Reference Document (Purchase Receipt Item)"),
|
||||||
|
)
|
||||||
|
|
||||||
def check_mandatory(self):
|
def check_mandatory(self):
|
||||||
if not self.get("purchase_receipts"):
|
if not self.get("purchase_receipts"):
|
||||||
frappe.throw(_("Please enter Receipt Document"))
|
frappe.throw(_("Please enter Receipt Document"))
|
||||||
|
|||||||
@@ -228,9 +228,17 @@ frappe.ui.form.on('Material Request', {
|
|||||||
const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty'];
|
const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty'];
|
||||||
|
|
||||||
if(!r.exc) {
|
if(!r.exc) {
|
||||||
$.each(r.message, function(k, v) {
|
$.each(r.message, function(key, value) {
|
||||||
if(!d[k] || in_list(qty_fields, k)) d[k] = v;
|
if(!d[key] || qty_fields.includes(key)) {
|
||||||
|
d[key] = value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (d.price_list_rate != r.message.price_list_rate) {
|
||||||
|
d.price_list_rate = r.message.price_list_rate;
|
||||||
|
|
||||||
|
frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -432,7 +440,6 @@ frappe.ui.form.on("Material Request Item", {
|
|||||||
item.amount = flt(item.qty) * flt(item.rate);
|
item.amount = flt(item.qty) * flt(item.rate);
|
||||||
frappe.model.set_value(doctype, name, "amount", item.amount);
|
frappe.model.set_value(doctype, name, "amount", item.amount);
|
||||||
refresh_field("amount", item.name, item.parentfield);
|
refresh_field("amount", item.name, item.parentfield);
|
||||||
frm.events.get_item_data(frm, item, false);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
item_code: function(frm, doctype, name) {
|
item_code: function(frm, doctype, name) {
|
||||||
@@ -452,7 +459,12 @@ frappe.ui.form.on("Material Request Item", {
|
|||||||
set_schedule_date(frm);
|
set_schedule_date(frm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
conversion_factor: function(frm, doctype, name) {
|
||||||
|
const item = locals[doctype][name];
|
||||||
|
frm.events.get_item_data(frm, item, false);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController {
|
erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"received_qty",
|
"received_qty",
|
||||||
"rate_and_amount_section_break",
|
"rate_and_amount_section_break",
|
||||||
"rate",
|
"rate",
|
||||||
|
"price_list_rate",
|
||||||
"col_break3",
|
"col_break3",
|
||||||
"amount",
|
"amount",
|
||||||
"accounting_details_section",
|
"accounting_details_section",
|
||||||
@@ -473,13 +474,22 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "WIP Composite Asset",
|
"label": "WIP Composite Asset",
|
||||||
"options": "Asset"
|
"options": "Asset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "price_list_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Price List Rate",
|
||||||
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-14 18:37:59.599115",
|
"modified": "2024-02-08 16:30:56.137858",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Material Request Item",
|
"name": "Material Request Item",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class MaterialRequestItem(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
price_list_rate: DF.Currency
|
||||||
production_plan: DF.Link | None
|
production_plan: DF.Link | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
projected_qty: DF.Float
|
projected_qty: DF.Float
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ frappe.ui.form.on('Pick List', {
|
|||||||
},
|
},
|
||||||
freeze: 1,
|
freeze: 1,
|
||||||
freeze_message: __("Setting Item Locations..."),
|
freeze_message: __("Setting Item Locations..."),
|
||||||
|
callback(r) {
|
||||||
|
refresh_field("locations");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"for_qty",
|
"for_qty",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"parent_warehouse",
|
"parent_warehouse",
|
||||||
|
"consider_rejected_warehouses",
|
||||||
"get_item_locations",
|
"get_item_locations",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"scan_barcode",
|
"scan_barcode",
|
||||||
@@ -184,11 +185,18 @@
|
|||||||
"report_hide": 1,
|
"report_hide": 1,
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Enable it if users want to consider rejected materials to dispatch.",
|
||||||
|
"fieldname": "consider_rejected_warehouses",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Consider Rejected Warehouses"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-01 16:17:44.877426",
|
"modified": "2024-02-02 16:17:44.877426",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List",
|
"name": "Pick List",
|
||||||
|
|||||||
@@ -348,9 +348,9 @@ class PickList(Document):
|
|||||||
picked_items_details = self.get_picked_items_details(items)
|
picked_items_details = self.get_picked_items_details(items)
|
||||||
self.item_location_map = frappe._dict()
|
self.item_location_map = frappe._dict()
|
||||||
|
|
||||||
from_warehouses = None
|
from_warehouses = [self.parent_warehouse] if self.parent_warehouse else []
|
||||||
if self.parent_warehouse:
|
if self.parent_warehouse:
|
||||||
from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
|
from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse))
|
||||||
|
|
||||||
# Create replica before resetting, to handle empty table on update after submit.
|
# Create replica before resetting, to handle empty table on update after submit.
|
||||||
locations_replica = self.get("locations")
|
locations_replica = self.get("locations")
|
||||||
@@ -369,6 +369,7 @@ class PickList(Document):
|
|||||||
self.item_count_map.get(item_code),
|
self.item_count_map.get(item_code),
|
||||||
self.company,
|
self.company,
|
||||||
picked_item_details=picked_items_details.get(item_code),
|
picked_item_details=picked_items_details.get(item_code),
|
||||||
|
consider_rejected_warehouses=self.consider_rejected_warehouses,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -710,6 +711,7 @@ def get_available_item_locations(
|
|||||||
company,
|
company,
|
||||||
ignore_validation=False,
|
ignore_validation=False,
|
||||||
picked_item_details=None,
|
picked_item_details=None,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
total_picked_qty = (
|
total_picked_qty = (
|
||||||
@@ -725,18 +727,34 @@ def get_available_item_locations(
|
|||||||
required_qty,
|
required_qty,
|
||||||
company,
|
company,
|
||||||
total_picked_qty,
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
elif has_serial_no:
|
elif has_serial_no:
|
||||||
locations = get_available_item_locations_for_serialized_item(
|
locations = get_available_item_locations_for_serialized_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
elif has_batch_no:
|
elif has_batch_no:
|
||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
locations = get_available_item_locations_for_other_item(
|
locations = get_available_item_locations_for_other_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
|
|
||||||
total_qty_available = sum(location.get("qty") for location in locations)
|
total_qty_available = sum(location.get("qty") for location in locations)
|
||||||
@@ -775,6 +793,7 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
required_qty,
|
required_qty,
|
||||||
company,
|
company,
|
||||||
total_picked_qty=0,
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
# Get batch nos by FIFO
|
# Get batch nos by FIFO
|
||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
@@ -782,6 +801,7 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
from_warehouses,
|
from_warehouses,
|
||||||
required_qty,
|
required_qty,
|
||||||
company,
|
company,
|
||||||
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
@@ -811,7 +831,12 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_serialized_item(
|
def get_available_item_locations_for_serialized_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
|
||||||
|
|
||||||
@@ -828,6 +853,10 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
else:
|
else:
|
||||||
query = query.where(Coalesce(sn.warehouse, "") != "")
|
query = query.where(Coalesce(sn.warehouse, "") != "")
|
||||||
|
|
||||||
|
if not consider_rejected_warehouses:
|
||||||
|
if rejected_warehouses := get_rejected_warehouses():
|
||||||
|
query = query.where(sn.warehouse.notin(rejected_warehouses))
|
||||||
|
|
||||||
serial_nos = query.run(as_list=True)
|
serial_nos = query.run(as_list=True)
|
||||||
|
|
||||||
warehouse_serial_nos_map = frappe._dict()
|
warehouse_serial_nos_map = frappe._dict()
|
||||||
@@ -860,7 +889,12 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_batched_item(
|
def get_available_item_locations_for_batched_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
data = get_auto_batch_nos(
|
data = get_auto_batch_nos(
|
||||||
@@ -875,7 +909,14 @@ def get_available_item_locations_for_batched_item(
|
|||||||
)
|
)
|
||||||
|
|
||||||
warehouse_wise_batches = frappe._dict()
|
warehouse_wise_batches = frappe._dict()
|
||||||
|
rejected_warehouses = get_rejected_warehouses()
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
|
if (
|
||||||
|
not consider_rejected_warehouses and rejected_warehouses and d.warehouse in rejected_warehouses
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
if d.warehouse not in warehouse_wise_batches:
|
if d.warehouse not in warehouse_wise_batches:
|
||||||
warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
|
warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
|
||||||
|
|
||||||
@@ -898,7 +939,12 @@ def get_available_item_locations_for_batched_item(
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_other_item(
|
def get_available_item_locations_for_other_item(
|
||||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
item_code,
|
||||||
|
from_warehouses,
|
||||||
|
required_qty,
|
||||||
|
company,
|
||||||
|
total_picked_qty=0,
|
||||||
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
bin = frappe.qb.DocType("Bin")
|
bin = frappe.qb.DocType("Bin")
|
||||||
query = (
|
query = (
|
||||||
@@ -915,6 +961,10 @@ def get_available_item_locations_for_other_item(
|
|||||||
wh = frappe.qb.DocType("Warehouse")
|
wh = frappe.qb.DocType("Warehouse")
|
||||||
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
|
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
|
||||||
|
|
||||||
|
if not consider_rejected_warehouses:
|
||||||
|
if rejected_warehouses := get_rejected_warehouses():
|
||||||
|
query = query.where(bin.warehouse.notin(rejected_warehouses))
|
||||||
|
|
||||||
item_locations = query.run(as_dict=True)
|
item_locations = query.run(as_dict=True)
|
||||||
|
|
||||||
return item_locations
|
return item_locations
|
||||||
@@ -1236,3 +1286,15 @@ def update_common_item_properties(item, location):
|
|||||||
item.serial_no = location.serial_no
|
item.serial_no = location.serial_no
|
||||||
item.batch_no = location.batch_no
|
item.batch_no = location.batch_no
|
||||||
item.material_request_item = location.material_request_item
|
item.material_request_item = location.material_request_item
|
||||||
|
|
||||||
|
|
||||||
|
def get_rejected_warehouses():
|
||||||
|
if not hasattr(frappe.local, "rejected_warehouses"):
|
||||||
|
frappe.local.rejected_warehouses = []
|
||||||
|
|
||||||
|
if not frappe.local.rejected_warehouses:
|
||||||
|
frappe.local.rejected_warehouses = frappe.get_all(
|
||||||
|
"Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name"
|
||||||
|
)
|
||||||
|
|
||||||
|
return frappe.local.rejected_warehouses
|
||||||
|
|||||||
@@ -684,9 +684,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
stock_value_diff = (
|
stock_value_diff = (
|
||||||
flt(d.base_net_amount)
|
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
|
||||||
+ flt(d.item_tax_amount / self.conversion_rate)
|
|
||||||
+ flt(d.landed_cost_voucher_amount)
|
|
||||||
)
|
)
|
||||||
elif warehouse_account.get(d.warehouse):
|
elif warehouse_account.get(d.warehouse):
|
||||||
stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
|
stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
|
from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
|
||||||
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
@@ -521,6 +521,24 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
make_serial_nos(item_code, serial_nos)
|
make_serial_nos(item_code, serial_nos)
|
||||||
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
|
self.assertTrue(frappe.db.exists("Serial No", serial_no_id))
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1})
|
||||||
|
def test_duplicate_serial_and_batch_bundle(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name
|
||||||
|
|
||||||
|
serial_no = f"{item_code}-001"
|
||||||
|
serial_nos = [{"serial_no": serial_no, "qty": 1}]
|
||||||
|
make_serial_nos(item_code, serial_nos)
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no])
|
||||||
|
pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True)
|
||||||
|
|
||||||
|
pr1.reload()
|
||||||
|
pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle
|
||||||
|
|
||||||
|
self.assertRaises(frappe.exceptions.ValidationError, pr2.save)
|
||||||
|
|
||||||
|
|
||||||
def get_batch_from_bundle(bundle):
|
def get_batch_from_bundle(bundle):
|
||||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||||
|
|||||||
@@ -1025,6 +1025,9 @@ class StockEntry(StockController):
|
|||||||
already_picked_serial_nos = []
|
already_picked_serial_nos = []
|
||||||
|
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
|
if row.use_serial_batch_fields and (row.serial_no or row.batch_no):
|
||||||
|
continue
|
||||||
|
|
||||||
if not row.s_warehouse:
|
if not row.s_warehouse:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1032,7 +1035,7 @@ class StockEntry(StockController):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
bundle_doc = None
|
bundle_doc = None
|
||||||
if row.serial_and_batch_bundle and abs(row.qty) != abs(
|
if row.serial_and_batch_bundle and abs(row.transfer_qty) != abs(
|
||||||
frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty")
|
frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty")
|
||||||
):
|
):
|
||||||
bundle_doc = SerialBatchCreation(
|
bundle_doc = SerialBatchCreation(
|
||||||
@@ -1042,7 +1045,7 @@ class StockEntry(StockController):
|
|||||||
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
||||||
"type_of_transaction": "Outward",
|
"type_of_transaction": "Outward",
|
||||||
"ignore_serial_nos": already_picked_serial_nos,
|
"ignore_serial_nos": already_picked_serial_nos,
|
||||||
"qty": row.qty * -1,
|
"qty": row.transfer_qty * -1,
|
||||||
}
|
}
|
||||||
).update_serial_and_batch_entries()
|
).update_serial_and_batch_entries()
|
||||||
elif not row.serial_and_batch_bundle:
|
elif not row.serial_and_batch_bundle:
|
||||||
@@ -1054,7 +1057,7 @@ class StockEntry(StockController):
|
|||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
"voucher_detail_no": row.name,
|
"voucher_detail_no": row.name,
|
||||||
"qty": row.qty * -1,
|
"qty": row.transfer_qty * -1,
|
||||||
"ignore_serial_nos": already_picked_serial_nos,
|
"ignore_serial_nos": already_picked_serial_nos,
|
||||||
"type_of_transaction": "Outward",
|
"type_of_transaction": "Outward",
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
|
|||||||
@@ -92,9 +92,6 @@ def make_stock_entry(**args):
|
|||||||
else:
|
else:
|
||||||
args.qty = cint(args.qty)
|
args.qty = cint(args.qty)
|
||||||
|
|
||||||
if args.serial_no or args.batch_no:
|
|
||||||
args.use_serial_batch_fields = True
|
|
||||||
|
|
||||||
# purpose
|
# purpose
|
||||||
if not args.purpose:
|
if not args.purpose:
|
||||||
if args.source and args.target:
|
if args.source and args.target:
|
||||||
@@ -136,7 +133,7 @@ def make_stock_entry(**args):
|
|||||||
serial_number = args.serial_no
|
serial_number = args.serial_no
|
||||||
|
|
||||||
bundle_id = None
|
bundle_id = None
|
||||||
if args.serial_no or args.batch_no or args.batches:
|
if not args.use_serial_batch_fields and (args.serial_no or args.batch_no or args.batches):
|
||||||
batches = frappe._dict({})
|
batches = frappe._dict({})
|
||||||
if args.batch_no:
|
if args.batch_no:
|
||||||
batches = frappe._dict({args.batch_no: args.qty})
|
batches = frappe._dict({args.batch_no: args.qty})
|
||||||
@@ -164,7 +161,11 @@ def make_stock_entry(**args):
|
|||||||
.name
|
.name
|
||||||
)
|
)
|
||||||
|
|
||||||
args.serial_no = serial_number
|
args["serial_no"] = ""
|
||||||
|
args["batch_no"] = ""
|
||||||
|
|
||||||
|
else:
|
||||||
|
args.serial_no = serial_number
|
||||||
|
|
||||||
s.append(
|
s.append(
|
||||||
"items",
|
"items",
|
||||||
|
|||||||
@@ -1587,6 +1587,7 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
qty=4,
|
qty=4,
|
||||||
to_warehouse="_Test Warehouse - _TC",
|
to_warehouse="_Test Warehouse - _TC",
|
||||||
batch_no=batch.name,
|
batch_no=batch.name,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1745,6 +1746,51 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
mr.cancel()
|
mr.cancel()
|
||||||
mr.delete()
|
mr.delete()
|
||||||
|
|
||||||
|
def test_use_serial_and_batch_fields(self):
|
||||||
|
item = make_item(
|
||||||
|
"Test Use Serial and Batch Item SN Item",
|
||||||
|
{"has_serial_no": 1, "is_stock_item": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = [
|
||||||
|
"Test Use Serial and Batch Item SN Item - SN 001",
|
||||||
|
"Test Use Serial and Batch Item SN Item - SN 002",
|
||||||
|
]
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item.name,
|
||||||
|
qty=2,
|
||||||
|
to_warehouse="_Test Warehouse - _TC",
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
serial_no="\n".join(serial_nos),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(se.items[0].use_serial_batch_fields)
|
||||||
|
self.assertFalse(se.items[0].serial_no)
|
||||||
|
self.assertTrue(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
for serial_no in serial_nos:
|
||||||
|
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")
|
||||||
|
|
||||||
|
se1 = make_stock_entry(
|
||||||
|
item_code=item.name,
|
||||||
|
qty=2,
|
||||||
|
from_warehouse="_Test Warehouse - _TC",
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
serial_no="\n".join(serial_nos),
|
||||||
|
)
|
||||||
|
|
||||||
|
se1.reload()
|
||||||
|
|
||||||
|
self.assertTrue(se1.items[0].use_serial_batch_fields)
|
||||||
|
self.assertFalse(se1.items[0].serial_no)
|
||||||
|
self.assertTrue(se1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
for serial_no in serial_nos:
|
||||||
|
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
||||||
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||||
|
|
||||||
|
|
||||||
def make_serialized_item(**args):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -315,7 +315,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-19 16:41:16.545416",
|
"modified": "2024-02-07 16:05:17.772098",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reservation Entry",
|
"name": "Stock Reservation Entry",
|
||||||
@@ -335,6 +335,90 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"is_group",
|
"is_group",
|
||||||
"parent_warehouse",
|
"parent_warehouse",
|
||||||
|
"is_rejected_warehouse",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"account",
|
"account",
|
||||||
"company",
|
"company",
|
||||||
@@ -249,13 +250,20 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_qajx",
|
"fieldname": "column_break_qajx",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If yes, then this warehouse will be used to store rejected materials",
|
||||||
|
"fieldname": "is_rejected_warehouse",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Rejected Warehouse"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-building",
|
"icon": "fa fa-building",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-29 13:10:43.333160",
|
"modified": "2024-01-24 16:27:28.299520",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Warehouse",
|
"name": "Warehouse",
|
||||||
|
|||||||
Reference in New Issue
Block a user