mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-12 09:52:29 +00:00
Compare commits
170 Commits
v15.12.2
...
cache_jour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f44ba1c6 | ||
|
|
014d21a050 | ||
|
|
05e4d1c240 | ||
|
|
4513d83f22 | ||
|
|
b785901efa | ||
|
|
b398cc6579 | ||
|
|
85ae9eee25 | ||
|
|
70ab25a611 | ||
|
|
0d8358bdd1 | ||
|
|
13e334398e | ||
|
|
1947a67f3d | ||
|
|
8720115f01 | ||
|
|
a5b3c1e7cb | ||
|
|
23d7a1fc76 | ||
|
|
c7ca929ddb | ||
|
|
4f9b194fe9 | ||
|
|
cda1c5ca10 | ||
|
|
9cb8d33923 | ||
|
|
6352bfe34e | ||
|
|
930df21a5f | ||
|
|
6a63a6c98a | ||
|
|
7e43f6b7e0 | ||
|
|
84a359c760 | ||
|
|
7d14ecfcb8 | ||
|
|
07fd93a6f6 | ||
|
|
4784117a8f | ||
|
|
7a9beb4d47 | ||
|
|
87f36ce425 | ||
|
|
48992cd205 | ||
|
|
99d1f1d67b | ||
|
|
eadea0207c | ||
|
|
e67d579ac4 | ||
|
|
bac2f66344 | ||
|
|
e7e8149fbe | ||
|
|
36b442a951 | ||
|
|
65853da505 | ||
|
|
df7afc3154 | ||
|
|
341f9030f5 | ||
|
|
6b5e1cfeb7 | ||
|
|
9f8a27833a | ||
|
|
1860399ccb | ||
|
|
57bb031602 | ||
|
|
9d19ec43c8 | ||
|
|
0694fd19fd | ||
|
|
94bb6241ef | ||
|
|
4d1f56c4bd | ||
|
|
ad53633c66 | ||
|
|
2bf6125b71 | ||
|
|
e68d2d1c7a | ||
|
|
93f64396ac | ||
|
|
91c21d13e5 | ||
|
|
38abfdb8ae | ||
|
|
194f46be57 | ||
|
|
d541ba3e61 | ||
|
|
27703b54bd | ||
|
|
9ffbf5623a | ||
|
|
56ee843233 | ||
|
|
383dfdd4ab | ||
|
|
5c269a9947 | ||
|
|
cf5fa210bb | ||
|
|
53d943ec70 | ||
|
|
d755b9d38c | ||
|
|
824df72eeb | ||
|
|
e4baacd79e | ||
|
|
38b384dfa2 | ||
|
|
10fe201bc8 | ||
|
|
bcb280c5e8 | ||
|
|
83d7111649 | ||
|
|
cd42089e20 | ||
|
|
3b96aaeead | ||
|
|
83b4cc5091 | ||
|
|
a75a69a01e | ||
|
|
190bd45bd7 | ||
|
|
c752bec2d9 | ||
|
|
8fef484f0f | ||
|
|
b40baf5e63 | ||
|
|
2c8ba892ac | ||
|
|
2e07b03307 | ||
|
|
0be520331c | ||
|
|
72da3083e6 | ||
|
|
a3ddd326d6 | ||
|
|
1c5a7e29f2 | ||
|
|
4269ef8c98 | ||
|
|
a543bf47ef | ||
|
|
c964c45f4e | ||
|
|
6f364df48c | ||
|
|
eaa3849df4 | ||
|
|
9f8f3db953 | ||
|
|
43c218c673 | ||
|
|
32d9642379 | ||
|
|
b9181e85dc | ||
|
|
66a05087b8 | ||
|
|
ca03e9cfd3 | ||
|
|
692867427c | ||
|
|
a0c0ab7709 | ||
|
|
ee2d108bef | ||
|
|
8eb2f67910 | ||
|
|
ed553b89a4 | ||
|
|
2ee51d36ff | ||
|
|
0f87ec15ad | ||
|
|
720144898f | ||
|
|
46f7569a54 | ||
|
|
a102172982 | ||
|
|
b954bdfdf9 | ||
|
|
9c01a5f7ed | ||
|
|
0c9d7434f9 | ||
|
|
2db79cdf3b | ||
|
|
acd2e93f8c | ||
|
|
c81a15eac3 | ||
|
|
18fcbb6f97 | ||
|
|
baa83ece21 | ||
|
|
74819b8e70 | ||
|
|
a2a5b34099 | ||
|
|
ce11180793 | ||
|
|
0b7a95351a | ||
|
|
d225508ab1 | ||
|
|
d213db90af | ||
|
|
f5d7fbdaf8 | ||
|
|
fd76847cfa | ||
|
|
709faaac69 | ||
|
|
3cd315973c | ||
|
|
15135952fc | ||
|
|
6d6a3a10fb | ||
|
|
7af62fb6c8 | ||
|
|
72ff56be13 | ||
|
|
b91f65d4cf | ||
|
|
40d4e3261e | ||
|
|
bf5d2f5fe4 | ||
|
|
bb77f85d89 | ||
|
|
37329469c3 | ||
|
|
ab03ab388c | ||
|
|
c50639334f | ||
|
|
518b22bffc | ||
|
|
a56d5b805c | ||
|
|
bef38f74fe | ||
|
|
14fe0af887 | ||
|
|
c78d085e24 | ||
|
|
8c12b3dbe0 | ||
|
|
88a7248888 | ||
|
|
a548f12941 | ||
|
|
f08b424972 | ||
|
|
375859a25f | ||
|
|
5a66c8585c | ||
|
|
a2f1a964f1 | ||
|
|
92e6017a29 | ||
|
|
10f17dfcc8 | ||
|
|
c643e70e2f | ||
|
|
4daee6d9c7 | ||
|
|
a29468f6fe | ||
|
|
beb4137dac | ||
|
|
2d5f186812 | ||
|
|
43fce29a04 | ||
|
|
63b4d20bdf | ||
|
|
b625b05ddc | ||
|
|
30daccecc7 | ||
|
|
db4efd3332 | ||
|
|
7556457e3e | ||
|
|
5590b04c89 | ||
|
|
490cbc53d8 | ||
|
|
592b3ff7b7 | ||
|
|
0bde22fe46 | ||
|
|
60e04ab661 | ||
|
|
83bf2c17e0 | ||
|
|
a805796c0a | ||
|
|
0bff065f0b | ||
|
|
5fd4ca56f5 | ||
|
|
a5ad318a37 | ||
|
|
ba05648741 | ||
|
|
3c6114ab72 | ||
|
|
22138867f5 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.12.2"
|
||||
__version__ = "15.14.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -56,7 +56,9 @@
|
||||
"Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {},
|
||||
"Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {},
|
||||
"Im\u00f3veis Destinados \u00e0 Venda": {},
|
||||
"Insumos (materiais diretos)": {},
|
||||
"Insumos (materiais diretos)": {
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Insumos Agropecu\u00e1rios": {},
|
||||
"Mercadorias para Revenda": {},
|
||||
"Outras 11": {},
|
||||
@@ -146,6 +148,65 @@
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"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 PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": {
|
||||
"Alimenta\u00e7\u00e3o do Trabalhador": {},
|
||||
@@ -621,7 +682,9 @@
|
||||
"Receita das Unidades Imobili\u00e1rias Vendidas": {},
|
||||
"Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -645,65 +708,6 @@
|
||||
}
|
||||
},
|
||||
"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 1": {
|
||||
"DESPESAS OPERACIONAIS 2": {
|
||||
|
||||
@@ -57,7 +57,9 @@ class BankAccount(Document):
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
|
||||
@@ -94,10 +94,13 @@ class BankTransaction(Document):
|
||||
pe.append(reference)
|
||||
|
||||
def update_allocated_amount(self):
|
||||
self.allocated_amount = (
|
||||
allocated_amount = (
|
||||
sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
|
||||
)
|
||||
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
|
||||
unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - allocated_amount
|
||||
|
||||
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
|
||||
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount"))
|
||||
|
||||
def before_submit(self):
|
||||
self.allocate_payment_entries()
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
from frappe.utils.caching import redis_cache
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||
@@ -1478,6 +1479,7 @@ def get_outstanding(args):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@redis_cache(ttl=5 * 60, user=True)
|
||||
def get_party_account_and_balance(company, party_type, party, cost_center=None):
|
||||
if not frappe.has_permission("Account"):
|
||||
frappe.msgprint(_("No Permission"), raise_exception=1)
|
||||
|
||||
@@ -149,7 +149,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
erpnext.hide_company();
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
|
||||
@@ -389,7 +389,10 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def set_missing_ref_details(
|
||||
self, force: bool = False, update_ref_details_only_for: list | None = None
|
||||
self,
|
||||
force: bool = False,
|
||||
update_ref_details_only_for: list | None = None,
|
||||
ref_exchange_rate: float | None = None,
|
||||
) -> None:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
@@ -401,6 +404,8 @@ class PaymentEntry(AccountsController):
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
)
|
||||
if ref_exchange_rate:
|
||||
ref_details.update({"exchange_rate": ref_exchange_rate})
|
||||
|
||||
for field, value in ref_details.items():
|
||||
if d.exchange_gain_loss:
|
||||
|
||||
@@ -631,7 +631,12 @@ class PaymentReconciliation(Document):
|
||||
journals_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
|
||||
filters={
|
||||
"parent": ("in", journals),
|
||||
"account": ("in", [self.receivable_payable_account]),
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
},
|
||||
fields=[
|
||||
"parent as `name`",
|
||||
"exchange_rate",
|
||||
|
||||
@@ -56,6 +56,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.expense_account = "Cost of Goods Sold - _PR"
|
||||
self.debit_to = "Debtors - _PR"
|
||||
self.creditors = "Creditors - _PR"
|
||||
self.cash = "Cash - _PR"
|
||||
|
||||
# create bank account
|
||||
if frappe.db.exists("Account", "HDFC - _PR"):
|
||||
@@ -486,6 +487,91 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_payment_against_foreign_currency_journal(self):
|
||||
transaction_date = nowdate()
|
||||
|
||||
self.supplier = "_Test Supplier USD"
|
||||
self.supplier2 = make_supplier("_Test Supplier2 USD", "USD")
|
||||
amount = 100
|
||||
exc_rate1 = 80
|
||||
exc_rate2 = 83
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = transaction_date
|
||||
je.company = self.company
|
||||
je.user_remark = "test"
|
||||
je.multi_currency = 1
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": self.creditors_usd,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"exchange_rate": exc_rate1,
|
||||
"cost_center": self.cost_center,
|
||||
"credit": amount * exc_rate1,
|
||||
"credit_in_account_currency": amount,
|
||||
},
|
||||
{
|
||||
"account": self.creditors_usd,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier2,
|
||||
"exchange_rate": exc_rate2,
|
||||
"cost_center": self.cost_center,
|
||||
"credit": amount * exc_rate2,
|
||||
"credit_in_account_currency": amount,
|
||||
},
|
||||
{
|
||||
"account": self.expense_account,
|
||||
"cost_center": self.cost_center,
|
||||
"debit": (amount * exc_rate1) + (amount * exc_rate2),
|
||||
"debit_in_account_currency": (amount * exc_rate1) + (amount * exc_rate2),
|
||||
},
|
||||
],
|
||||
)
|
||||
je.save().submit()
|
||||
|
||||
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
pe.payment_type = "Pay"
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_amount = 8000
|
||||
pe.received_amount = 100
|
||||
pe.target_exchange_rate = exc_rate1
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
|
||||
pr.from_invoice_date = pr.to_invoice_date = transaction_date
|
||||
pr.from_payment_date = pr.to_payment_date = transaction_date
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# There should no difference_amount as the Journal and Payment have same exchange rate - 'exc_rate1'
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": je.doctype, "reference_name": je.name, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
)
|
||||
self.assertEqual([], journals)
|
||||
|
||||
def test_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
@@ -591,6 +677,70 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_invoice_status_after_cr_note_cancellation(self):
|
||||
# This test case is made after the 'always standalone Credit/Debit notes' feature is introduced
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note.return_against = si.name
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"is_system_generated": 1,
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Credit Note",
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
self.assertEqual(len(journals), 1)
|
||||
|
||||
# assert status and outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Credit Note Issued")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
cr_note.reload()
|
||||
cr_note.cancel()
|
||||
# 'Credit Note' Journal should be auto cancelled
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"is_system_generated": 1,
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Credit Note",
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
self.assertEqual(len(journals), 0)
|
||||
# assert status and outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Unpaid")
|
||||
self.assertEqual(si.outstanding_amount, 100)
|
||||
|
||||
def test_cr_note_partial_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
@@ -1184,3 +1334,17 @@ def make_customer(customer_name, currency=None):
|
||||
return customer.name
|
||||
else:
|
||||
return customer_name
|
||||
|
||||
|
||||
def make_supplier(supplier_name, currency=None):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
supplier.type = "Individual"
|
||||
|
||||
if currency:
|
||||
supplier.default_currency = currency
|
||||
supplier.save()
|
||||
return supplier.name
|
||||
else:
|
||||
return supplier_name
|
||||
|
||||
@@ -654,7 +654,7 @@
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
@@ -853,7 +853,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 16:36:25.665743",
|
||||
"modified": "2024-02-25 15:50:17.140269",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
|
||||
@@ -72,7 +72,7 @@ class POSInvoiceItem(Document):
|
||||
rate_with_margin: DF.Currency
|
||||
sales_order: DF.Link | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.SmallText | None
|
||||
serial_no: DF.Text | None
|
||||
service_end_date: DF.Date | None
|
||||
service_start_date: DF.Date | None
|
||||
service_stop_date: DF.Date | None
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"is_paid",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"update_billed_amount_in_purchase_order",
|
||||
"update_billed_amount_in_purchase_receipt",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"amended_from",
|
||||
@@ -412,6 +414,20 @@
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_purchase_order",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Purchase Order"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_purchase_receipt",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Purchase Receipt"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_addresses",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1612,7 +1628,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 15:35:44.697496",
|
||||
"modified": "2024-02-25 11:20:28.366808",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1675,4 +1691,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,8 @@ class PurchaseInvoice(BuyingController):
|
||||
total_qty: DF.Float
|
||||
total_taxes_and_charges: DF.Currency
|
||||
unrealized_profit_loss_account: DF.Link | None
|
||||
update_billed_amount_in_purchase_order: DF.Check
|
||||
update_billed_amount_in_purchase_receipt: DF.Check
|
||||
update_stock: DF.Check
|
||||
use_company_roundoff_cost_center: DF.Check
|
||||
use_transaction_date_exchange_rate: DF.Check
|
||||
@@ -679,6 +681,11 @@ class PurchaseInvoice(BuyingController):
|
||||
super(PurchaseInvoice, self).on_submit()
|
||||
|
||||
self.check_prev_docstatus()
|
||||
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_order:
|
||||
# NOTE status updating bypassed for is_return
|
||||
self.status_updater = []
|
||||
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
|
||||
@@ -1426,6 +1433,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.check_on_hold_or_closed_status()
|
||||
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_order:
|
||||
# NOTE status updating bypassed for is_return
|
||||
self.status_updater = []
|
||||
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
|
||||
@@ -1520,6 +1531,9 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
|
||||
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
||||
return
|
||||
|
||||
updated_pr = []
|
||||
po_details = []
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ class RepostAccountingLedger(Document):
|
||||
).append(gle.update({"old": True}))
|
||||
|
||||
def generate_preview_data(self):
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
self.gl_entries = []
|
||||
self.get_existing_ledger_entries()
|
||||
for x in self.vouchers:
|
||||
@@ -141,6 +142,7 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
|
||||
|
||||
@@ -14,23 +14,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.setup(doc);
|
||||
}
|
||||
company() {
|
||||
super.company();
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
|
||||
let me = this;
|
||||
if (this.frm.doc.company) {
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
party_type: 'Customer',
|
||||
party: this.frm.doc.customer,
|
||||
company: this.frm.doc.company
|
||||
},
|
||||
callback: (response) => {
|
||||
if (response) me.frm.set_value("debit_to", response.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
onload() {
|
||||
var me = this;
|
||||
|
||||
@@ -269,7 +269,7 @@ class SalesInvoice(SellingController):
|
||||
super(SalesInvoice, self).validate()
|
||||
self.validate_auto_set_posting_time()
|
||||
|
||||
if not self.is_pos:
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
|
||||
self.set_tax_withholding()
|
||||
@@ -446,7 +446,11 @@ class SalesInvoice(SellingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
for table_name in ["items", "packed_items"]:
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -1472,9 +1476,7 @@ class SalesInvoice(SellingController):
|
||||
"credit_in_account_currency": payment_mode.base_amount
|
||||
if self.party_account_currency == self.company_currency
|
||||
else payment_mode.amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
|
||||
@@ -1088,6 +1088,44 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(pos.grand_total, 100.0)
|
||||
self.assertEqual(pos.write_off_amount, 10)
|
||||
|
||||
def test_ledger_entries_of_return_pos_invoice(self):
|
||||
make_pos_profile()
|
||||
|
||||
pos = create_sales_invoice(do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos.save().submit()
|
||||
self.assertEqual(pos.outstanding_amount, 0.0)
|
||||
self.assertEqual(pos.status, "Paid")
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_return = make_sales_return(pos.name)
|
||||
pos_return.save().submit()
|
||||
pos_return.reload()
|
||||
pos.reload()
|
||||
self.assertEqual(pos_return.is_return, 1)
|
||||
self.assertEqual(pos_return.return_against, pos.name)
|
||||
self.assertEqual(pos_return.outstanding_amount, 0.0)
|
||||
self.assertEqual(pos_return.status, "Return")
|
||||
self.assertEqual(pos.outstanding_amount, 0.0)
|
||||
self.assertEqual(pos.status, "Credit Note Issued")
|
||||
|
||||
expected = (
|
||||
("Cash - _TC", 0.0, 100.0, pos_return.name, None),
|
||||
("Debtors - _TC", 0.0, 100.0, pos_return.name, pos_return.name),
|
||||
("Debtors - _TC", 100.0, 0.0, pos_return.name, pos_return.name),
|
||||
("Sales - _TC", 100.0, 0.0, pos_return.name, None),
|
||||
)
|
||||
res = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pos_return.name, "is_cancelled": 0},
|
||||
fields=["account", "debit", "credit", "voucher_no", "against_voucher"],
|
||||
order_by="account, debit, credit",
|
||||
as_list=1,
|
||||
)
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
def test_pos_with_no_gl_entry_for_change_amount(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 0)
|
||||
|
||||
@@ -3564,6 +3602,33 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
|
||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||
|
||||
def test_pulling_advance_based_on_debit_to(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
|
||||
debtors2 = create_account(
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
account_name="Debtors 2",
|
||||
company="_Test Company",
|
||||
account_type="Receivable",
|
||||
)
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
si.debit_to = debtors2
|
||||
si.save()
|
||||
|
||||
pe = create_payment_entry(
|
||||
company=si.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=si.customer,
|
||||
paid_from=debtors2,
|
||||
paid_to="Cash - _TC",
|
||||
paid_amount=1000,
|
||||
)
|
||||
pe.submit()
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(1, len(advances))
|
||||
self.assertEqual(advances[0].reference_name, pe.name)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -625,7 +625,7 @@
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
"oldfieldtype": "Small Text"
|
||||
@@ -926,7 +926,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 11:52:16.106541",
|
||||
"modified": "2024-02-25 15:56:44.828634",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -75,7 +75,7 @@ class SalesInvoiceItem(Document):
|
||||
sales_invoice_item: DF.Data | None
|
||||
sales_order: DF.Link | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.SmallText | None
|
||||
serial_no: DF.Text | None
|
||||
service_end_date: DF.Date | None
|
||||
service_start_date: DF.Date | None
|
||||
service_stop_date: DF.Date | None
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import today
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -49,6 +50,16 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
)
|
||||
return pe
|
||||
|
||||
def create_sales_order(self):
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item=self.item,
|
||||
rate=100,
|
||||
transaction_date=today(),
|
||||
)
|
||||
return so
|
||||
|
||||
def test_01_unreconcile_invoice(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
@@ -314,3 +325,41 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_05_unreconcile_order(self):
|
||||
so = self.create_sales_order()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
# Allocation payment against Sales Order
|
||||
pe.paid_amount = 100
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 1)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([so.name], allocations)
|
||||
# unreconcile so
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
@@ -82,6 +82,9 @@ class UnreconcilePayment(Document):
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _, msgprint, qb, scrub
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Abs, Date, Sum
|
||||
from frappe.query_builder.functions import Abs, Count, Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -784,34 +784,37 @@ def get_timeline_data(doctype, name):
|
||||
from frappe.desk.form.load import get_communication_data
|
||||
|
||||
out = {}
|
||||
fields = "creation, count(*)"
|
||||
after = add_years(None, -1).strftime("%Y-%m-%d")
|
||||
group_by = "group by Date(creation)"
|
||||
|
||||
data = get_communication_data(
|
||||
doctype,
|
||||
name,
|
||||
after=after,
|
||||
group_by="group by creation",
|
||||
fields="C.creation as creation, count(C.name)",
|
||||
group_by="group by communication_date",
|
||||
fields="C.communication_date as communication_date, count(C.name)",
|
||||
as_dict=False,
|
||||
)
|
||||
|
||||
# fetch and append data from Activity Log
|
||||
data += frappe.db.sql(
|
||||
"""select {fields}
|
||||
from `tabActivity Log`
|
||||
where (reference_doctype=%(doctype)s and reference_name=%(name)s)
|
||||
or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s)
|
||||
or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s)
|
||||
and status!='Success' and creation > {after}
|
||||
{group_by} order by creation desc
|
||||
""".format(
|
||||
fields=fields, group_by=group_by, after=after
|
||||
),
|
||||
{"doctype": doctype, "name": name},
|
||||
as_dict=False,
|
||||
)
|
||||
activity_log = frappe.qb.DocType("Activity Log")
|
||||
data += (
|
||||
frappe.qb.from_(activity_log)
|
||||
.select(activity_log.communication_date, Count(activity_log.name))
|
||||
.where(
|
||||
(
|
||||
((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
|
||||
| ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
|
||||
| (
|
||||
(activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
|
||||
& (activity_log.timeline_name == name)
|
||||
)
|
||||
)
|
||||
& (activity_log.status != "Success")
|
||||
& (activity_log.creation > after)
|
||||
)
|
||||
.groupby(activity_log.communication_date)
|
||||
.orderby(activity_log.communication_date, order=frappe.qb.desc)
|
||||
).run()
|
||||
|
||||
timeline_items = dict(data)
|
||||
|
||||
|
||||
@@ -83,7 +83,10 @@ class ReceivablePayableReport(object):
|
||||
self.skip_total_row = 1
|
||||
|
||||
if self.filters.get("in_party_currency"):
|
||||
self.skip_total_row = 1
|
||||
if self.filters.get("party") and len(self.filters.get("party")) == 1:
|
||||
self.skip_total_row = 0
|
||||
else:
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
|
||||
@@ -975,7 +975,7 @@ class GrossProfitGenerator(object):
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.orderby(sle.item_code)
|
||||
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
|
||||
.orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -350,7 +350,13 @@ def get_conditions(filters, additional_conditions=None):
|
||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
||||
|
||||
if filters.get("warehouse"):
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
|
||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||
lft, rgt = frappe.db.get_all(
|
||||
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
||||
)[0]
|
||||
conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
|
||||
else:
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
|
||||
|
||||
if filters.get("brand"):
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""
|
||||
|
||||
@@ -163,7 +163,7 @@ def get_entries(filters):
|
||||
"""select
|
||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') {0}
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {0}
|
||||
""".format(
|
||||
get_conditions(filters)
|
||||
),
|
||||
|
||||
@@ -242,7 +242,7 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
|
||||
@@ -714,7 +714,7 @@ def update_reference_in_payment_entry(
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details()
|
||||
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
|
||||
payment_entry.set_amounts()
|
||||
payment_entry.make_exchange_gain_loss_journal(
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||
@@ -970,46 +970,6 @@ def get_currency_precision():
|
||||
return precision
|
||||
|
||||
|
||||
def get_stock_rbnb_difference(posting_date, company):
|
||||
stock_items = frappe.db.sql_list(
|
||||
"""select distinct item_code
|
||||
from `tabStock Ledger Entry` where company=%s""",
|
||||
company,
|
||||
)
|
||||
|
||||
pr_valuation_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor)
|
||||
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
|
||||
where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s
|
||||
and pr.posting_date <= %s and pr_item.item_code in (%s)"""
|
||||
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
|
||||
tuple([company, posting_date] + stock_items),
|
||||
)[0][0]
|
||||
|
||||
pi_valuation_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor)
|
||||
from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi
|
||||
where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s
|
||||
and pi.posting_date <= %s and pi_item.item_code in (%s)"""
|
||||
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
|
||||
tuple([company, posting_date] + stock_items),
|
||||
)[0][0]
|
||||
|
||||
# Balance should be
|
||||
stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2)
|
||||
|
||||
# Balance as per system
|
||||
stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value(
|
||||
"Company", company, "abbr"
|
||||
)
|
||||
sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False)
|
||||
|
||||
# Amount should be credited
|
||||
return flt(stock_rbnb) + flt(sys_bal)
|
||||
|
||||
|
||||
def get_held_invoices(party_type, party):
|
||||
"""
|
||||
Returns a list of names Purchase Invoices for the given party that are on hold
|
||||
@@ -1412,8 +1372,7 @@ def sort_stock_vouchers_by_posting_date(
|
||||
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
||||
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
||||
.groupby(sle.voucher_type, sle.voucher_no)
|
||||
.orderby(sle.posting_date)
|
||||
.orderby(sle.posting_time)
|
||||
.orderby(sle.posting_datetime)
|
||||
.orderby(sle.creation)
|
||||
).run(as_dict=True)
|
||||
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||
|
||||
@@ -561,13 +561,14 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
for row in asset.get("finance_books"):
|
||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
||||
if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"):
|
||||
continue
|
||||
|
||||
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
|
||||
if schedule.schedule_date == date:
|
||||
if schedule.schedule_date == date and schedule.journal_entry:
|
||||
if not disposal_was_made_on_original_schedule_date(
|
||||
schedule_idx, row, date
|
||||
) or disposal_happens_in_the_future(date):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ frappe.provide("erpnext.assets");
|
||||
|
||||
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
|
||||
setup() {
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle', 'Asset Movement'];
|
||||
this.setup_posting_date_time_check();
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ class AssetCapitalization(StockController):
|
||||
"Repost Item Valuation",
|
||||
"Serial and Batch Bundle",
|
||||
"Asset",
|
||||
"Asset Movement",
|
||||
)
|
||||
self.cancel_target_asset()
|
||||
self.update_stock_ledger()
|
||||
@@ -147,6 +148,7 @@ class AssetCapitalization(StockController):
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.db_set("capitalized_in", None)
|
||||
if asset_doc.docstatus == 1:
|
||||
asset_doc.cancel()
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Serial No",
|
||||
"print_hide": 1
|
||||
@@ -178,7 +178,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 16:41:09.239762",
|
||||
"modified": "2024-02-25 15:57:35.007501",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Stock Item",
|
||||
|
||||
@@ -24,7 +24,7 @@ class AssetCapitalizationStockItem(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.SmallText | None
|
||||
serial_no: DF.Text | None
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
|
||||
@@ -418,14 +418,13 @@ class AssetDepreciationSchedule(Document):
|
||||
)
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if row.expected_value_after_useful_life and (
|
||||
(
|
||||
n == cint(final_number_of_depreciations) - 1
|
||||
and value_after_depreciation != row.expected_value_after_useful_life
|
||||
if (
|
||||
n == cint(final_number_of_depreciations) - 1
|
||||
and flt(value_after_depreciation) != flt(row.expected_value_after_useful_life)
|
||||
) or flt(value_after_depreciation) < flt(row.expected_value_after_useful_life):
|
||||
depreciation_amount += flt(value_after_depreciation) - flt(
|
||||
row.expected_value_after_useful_life
|
||||
)
|
||||
or value_after_depreciation < row.expected_value_after_useful_life
|
||||
):
|
||||
depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
|
||||
skip_row = True
|
||||
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
@@ -813,15 +812,11 @@ def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
||||
asset_depr_schedules_names = []
|
||||
|
||||
for row in asset_doc.get("finance_books"):
|
||||
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
|
||||
asset_doc.name, "Draft", row.finance_book
|
||||
asset_depr_schedule = get_asset_depr_schedule_name(
|
||||
asset_doc.name, ["Draft", "Active"], row.finance_book
|
||||
)
|
||||
|
||||
active_asset_depr_schedule_name = get_asset_depr_schedule_name(
|
||||
asset_doc.name, "Active", row.finance_book
|
||||
)
|
||||
|
||||
if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name:
|
||||
if not asset_depr_schedule:
|
||||
name = make_draft_asset_depr_schedule(asset_doc, row)
|
||||
asset_depr_schedules_names.append(name)
|
||||
|
||||
@@ -997,16 +992,20 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
|
||||
|
||||
|
||||
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
|
||||
finance_book_filter = ["finance_book", "is", "not set"]
|
||||
if finance_book:
|
||||
if finance_book is None:
|
||||
finance_book_filter = ["finance_book", "is", "not set"]
|
||||
else:
|
||||
finance_book_filter = ["finance_book", "=", finance_book]
|
||||
|
||||
if isinstance(status, str):
|
||||
status = [status]
|
||||
|
||||
return frappe.db.get_value(
|
||||
doctype="Asset Depreciation Schedule",
|
||||
filters=[
|
||||
["asset", "=", asset_name],
|
||||
finance_book_filter,
|
||||
["status", "=", status],
|
||||
["status", "in", status],
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1037,6 +1037,38 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name}))
|
||||
|
||||
def test_po_billed_amount_against_return_entry(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
|
||||
|
||||
# Create a Purchase Order and Fully Bill it
|
||||
po = create_purchase_order()
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Debit Note - 50% Qty & enable updating PO billed amount
|
||||
pi_return = make_debit_note(pi.name)
|
||||
pi_return.items[0].qty = -5
|
||||
pi_return.update_billed_amount_in_purchase_order = 1
|
||||
pi_return.submit()
|
||||
|
||||
# Check if the billed amount reduced
|
||||
po.reload()
|
||||
self.assertEqual(po.per_billed, 50)
|
||||
|
||||
pi_return.reload()
|
||||
pi_return.cancel()
|
||||
|
||||
# Debit Note - 50% Qty & disable updating PO billed amount
|
||||
pi_return = make_debit_note(pi.name)
|
||||
pi_return.items[0].qty = -5
|
||||
pi_return.update_billed_amount_in_purchase_order = 0
|
||||
pi_return.submit()
|
||||
|
||||
# Check if the billed amount stayed the same
|
||||
po.reload()
|
||||
self.assertEqual(po.per_billed, 100)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -205,10 +205,30 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
contact.save(ignore_permissions=True)
|
||||
|
||||
if rfq_supplier.supplier:
|
||||
self.update_user_in_supplier(rfq_supplier.supplier, user.name)
|
||||
|
||||
if not rfq_supplier.contact:
|
||||
# return contact to later update, RFQ supplier row's contact
|
||||
return contact.name
|
||||
|
||||
def update_user_in_supplier(self, supplier, user):
|
||||
"""Update user in Supplier."""
|
||||
if not frappe.db.exists("Portal User", {"parent": supplier, "user": user}):
|
||||
supplier_doc = frappe.get_doc("Supplier", supplier)
|
||||
supplier_doc.append(
|
||||
"portal_users",
|
||||
{
|
||||
"user": user,
|
||||
},
|
||||
)
|
||||
|
||||
supplier_doc.flags.ignore_validate = True
|
||||
supplier_doc.flags.ignore_mandatory = True
|
||||
supplier_doc.flags.ignore_permissions = True
|
||||
|
||||
supplier_doc.save()
|
||||
|
||||
def create_user(self, rfq_supplier, link):
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
@@ -245,6 +265,10 @@ class RequestforQuotation(BuyingController):
|
||||
"user_fullname": full_name,
|
||||
}
|
||||
)
|
||||
|
||||
if not self.email_template:
|
||||
return
|
||||
|
||||
email_template = frappe.get_doc("Email Template", self.email_template)
|
||||
message = frappe.render_template(email_template.response_, doc_args)
|
||||
subject = frappe.render_template(email_template.subject, doc_args)
|
||||
|
||||
@@ -138,6 +138,33 @@ class TestRequestforQuotation(FrappeTestCase):
|
||||
get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
|
||||
self.assertEqual(frappe.local.response.type, "pdf")
|
||||
|
||||
def test_portal_user_with_new_supplier(self):
|
||||
supplier_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier",
|
||||
"supplier_name": "Test Supplier for RFQ",
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.assertFalse(supplier_doc.portal_users)
|
||||
|
||||
rfq = make_request_for_quotation(
|
||||
supplier_data=[
|
||||
{
|
||||
"supplier": supplier_doc.name,
|
||||
"supplier_name": supplier_doc.supplier_name,
|
||||
"email_id": "123_testrfquser@example.com",
|
||||
}
|
||||
],
|
||||
do_not_submit=True,
|
||||
)
|
||||
for rfq_supplier in rfq.suppliers:
|
||||
rfq.update_supplier_contact(rfq_supplier, rfq.get_link())
|
||||
|
||||
supplier_doc.reload()
|
||||
self.assertTrue(supplier_doc.portal_users[0].user)
|
||||
|
||||
|
||||
def make_request_for_quotation(**args) -> "RequestforQuotation":
|
||||
"""
|
||||
|
||||
@@ -216,6 +216,19 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
||||
# if self.get("is_return") and self.get("return_against"):
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
|
||||
).format(
|
||||
document_type,
|
||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
)
|
||||
)
|
||||
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
self.set_advances()
|
||||
@@ -333,6 +346,12 @@ class AccountsController(TransactionBase):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
frappe.qb.from_(ple).delete().where(
|
||||
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
|
||||
| (
|
||||
(ple.against_voucher_type == self.doctype)
|
||||
& (ple.against_voucher_no == self.name)
|
||||
& ple.delinked
|
||||
== 1
|
||||
)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
|
||||
@@ -1114,21 +1133,24 @@ class AccountsController(TransactionBase):
|
||||
self.append("advances", advance_row)
|
||||
|
||||
def get_advance_entries(self, include_unallocated=True):
|
||||
party_account = []
|
||||
if self.doctype == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party = self.customer
|
||||
amount_field = "credit_in_account_currency"
|
||||
order_field = "sales_order"
|
||||
order_doctype = "Sales Order"
|
||||
party_account.append(self.debit_to)
|
||||
else:
|
||||
party_type = "Supplier"
|
||||
party = self.supplier
|
||||
amount_field = "debit_in_account_currency"
|
||||
order_field = "purchase_order"
|
||||
order_doctype = "Purchase Order"
|
||||
party_account.append(self.credit_to)
|
||||
|
||||
party_account = get_party_account(
|
||||
party_type, party=party, company=self.company, include_advance=True
|
||||
party_account.extend(
|
||||
get_party_account(party_type, party=party, company=self.company, include_advance=True)
|
||||
)
|
||||
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
@@ -1472,6 +1494,24 @@ class AccountsController(TransactionBase):
|
||||
x.update({dim.fieldname: self.get(dim.fieldname)})
|
||||
reconcile_against_document(lst, active_dimensions=active_dimensions)
|
||||
|
||||
def cancel_system_generated_credit_debit_notes(self):
|
||||
# Cancel 'Credit/Debit' Note Journal Entries, if found.
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"is_system_generated": 1,
|
||||
"reference_type": self.doctype,
|
||||
"reference_name": self.name,
|
||||
"voucher_type": voucher_type,
|
||||
"docstatus": 1,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
for x in journals:
|
||||
frappe.get_doc("Journal Entry", x).cancel()
|
||||
|
||||
def on_cancel(self):
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
||||
remove_from_bank_transaction,
|
||||
@@ -1484,6 +1524,8 @@ class AccountsController(TransactionBase):
|
||||
remove_from_bank_transaction(self.doctype, self.name)
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
self.cancel_system_generated_credit_debit_notes()
|
||||
|
||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||
cancel_exchange_gain_loss_journal(self)
|
||||
|
||||
@@ -2631,7 +2673,7 @@ def get_advance_journal_entries(
|
||||
|
||||
if order_list:
|
||||
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)
|
||||
|
||||
@@ -217,8 +217,8 @@ class BuyingController(SubcontractingController):
|
||||
lc_voucher_data = frappe.db.sql(
|
||||
"""select sum(applicable_charges), cost_center
|
||||
from `tabLanded Cost Item`
|
||||
where docstatus = 1 and purchase_receipt_item = %s""",
|
||||
d.name,
|
||||
where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""",
|
||||
(d.name, self.name),
|
||||
)
|
||||
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]:
|
||||
@@ -824,7 +824,8 @@ class BuyingController(SubcontractingController):
|
||||
if self.doctype == "Purchase Invoice" and not self.get("update_stock"):
|
||||
return
|
||||
|
||||
frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name)
|
||||
asset_movement = frappe.db.get_value("Asset Movement", {"reference_name": self.name}, "name")
|
||||
frappe.delete_doc("Asset Movement", asset_movement, force=1)
|
||||
|
||||
def validate_schedule_date(self):
|
||||
if not self.get("items"):
|
||||
|
||||
@@ -28,7 +28,8 @@ class SellingController(StockController):
|
||||
def validate(self):
|
||||
super(SellingController, self).validate()
|
||||
self.validate_items()
|
||||
self.validate_max_discount()
|
||||
if not self.get("is_debit_note"):
|
||||
self.validate_max_discount()
|
||||
self.validate_selling_price()
|
||||
self.set_qty_as_per_stock_uom()
|
||||
self.set_po_nos(for_validate=True)
|
||||
@@ -706,6 +707,9 @@ def set_default_income_account_for_item(obj):
|
||||
def get_serial_and_batch_bundle(child, parent):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if child.get("use_serial_batch_fields"):
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
|
||||
):
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import (
|
||||
@@ -46,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError):
|
||||
class StockController(AccountsController):
|
||||
def validate(self):
|
||||
super(StockController, self).validate()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.validate_duplicate_serial_and_batch_bundle()
|
||||
if not self.get("is_return"):
|
||||
self.validate_inspection()
|
||||
self.validate_serialized_batch()
|
||||
@@ -55,6 +58,32 @@ class StockController(AccountsController):
|
||||
self.validate_internal_transfer()
|
||||
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):
|
||||
if self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
@@ -129,26 +158,34 @@ class StockController(AccountsController):
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
def make_bundle_using_old_serial_batch_fields(self):
|
||||
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if self.get("_action") == "update_after_submit":
|
||||
return
|
||||
|
||||
# To handle test cases
|
||||
if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
|
||||
return
|
||||
|
||||
table_name = "items"
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
|
||||
if self.doctype == "Asset Capitalization":
|
||||
table_name = "stock_items"
|
||||
|
||||
for row in self.get(table_name):
|
||||
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
|
||||
self.validate_serial_nos_and_batches_with_bundle(row)
|
||||
|
||||
if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
|
||||
continue
|
||||
|
||||
if not row.use_serial_batch_fields and (
|
||||
row.serial_no or row.batch_no or row.get("rejected_serial_no")
|
||||
):
|
||||
frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
|
||||
row.use_serial_batch_fields = 1
|
||||
|
||||
if row.use_serial_batch_fields and (
|
||||
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
|
||||
@@ -156,14 +193,24 @@ class StockController(AccountsController):
|
||||
if self.doctype == "Stock Reconciliation":
|
||||
qty = row.qty
|
||||
type_of_transaction = "Inward"
|
||||
warehouse = row.warehouse
|
||||
elif table_name == "packed_items":
|
||||
qty = row.qty
|
||||
warehouse = row.warehouse
|
||||
type_of_transaction = "Outward"
|
||||
if self.is_return:
|
||||
type_of_transaction = "Inward"
|
||||
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)
|
||||
warehouse = (
|
||||
row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse
|
||||
)
|
||||
|
||||
sn_doc = SerialBatchCreation(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"voucher_type": self.doctype,
|
||||
@@ -186,7 +233,6 @@ class StockController(AccountsController):
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": sn_doc.name,
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -194,11 +240,44 @@ class StockController(AccountsController):
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": sn_doc.name,
|
||||
"serial_no": "",
|
||||
"batch_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def validate_serial_nos_and_batches_with_bundle(self, row):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
throw_error = False
|
||||
if row.serial_no:
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["serial_no"], filters={"parent": row.serial_and_batch_bundle}
|
||||
)
|
||||
serial_nos = sorted([cstr(d.serial_no) for d in serial_nos])
|
||||
parsed_serial_nos = get_serial_nos(row.serial_no)
|
||||
|
||||
if len(serial_nos) != len(parsed_serial_nos):
|
||||
throw_error = True
|
||||
elif serial_nos != parsed_serial_nos:
|
||||
for serial_no in serial_nos:
|
||||
if serial_no not in parsed_serial_nos:
|
||||
throw_error = True
|
||||
break
|
||||
|
||||
elif row.batch_no:
|
||||
batches = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": row.serial_and_batch_bundle}
|
||||
)
|
||||
batches = sorted([d.batch_no for d in batches])
|
||||
|
||||
if batches != [row.batch_no]:
|
||||
throw_error = True
|
||||
|
||||
if throw_error:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields."
|
||||
).format(row.idx, row.serial_and_batch_bundle)
|
||||
)
|
||||
|
||||
def set_use_serial_batch_fields(self):
|
||||
if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
|
||||
for row in self.items:
|
||||
@@ -938,6 +1017,9 @@ class StockController(AccountsController):
|
||||
"Stock Reconciliation",
|
||||
)
|
||||
|
||||
if not frappe.get_all("Putaway Rule", limit=1):
|
||||
return
|
||||
|
||||
if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
|
||||
valid_doctype = False
|
||||
|
||||
|
||||
@@ -539,6 +539,10 @@ class SubcontractingController(StockController):
|
||||
def __add_supplied_item(self, item_row, bom_item, qty):
|
||||
bom_item.conversion_factor = item_row.conversion_factor
|
||||
rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
if rm_obj.get("qty"):
|
||||
# Qty field not exists
|
||||
rm_obj.qty = 0.0
|
||||
|
||||
rm_obj.reference_name = item_row.name
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
|
||||
@@ -1020,7 +1020,7 @@ def get_itemised_tax_breakup_data(doc):
|
||||
for item_code, taxes in itemised_tax.items():
|
||||
itemised_tax_data.append(
|
||||
frappe._dict(
|
||||
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes}
|
||||
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code, 0), **taxes}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1041,18 +1041,18 @@ class TestAccountsController(FrappeTestCase):
|
||||
cr_note.reload()
|
||||
cr_note.cancel()
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
# with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller
|
||||
# JE(Credit Note) will be cancelled once the parent is cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 0)
|
||||
self.assertEqual(len(exc_je_for_cr), 0)
|
||||
|
||||
# The Credit Note JE is still active and is referencing the sales invoice
|
||||
# So, outstanding stays the same
|
||||
# No references, full outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
self.assertEqual(si.outstanding_amount, 2)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||
|
||||
def test_40_cost_center_from_payment_entry(self):
|
||||
"""
|
||||
|
||||
@@ -401,7 +401,7 @@ class TestSubcontractingController(FrappeTestCase):
|
||||
{
|
||||
"main_item_code": "Subcontracted Item SA4",
|
||||
"item_code": "Subcontracted SRM Item 3",
|
||||
"qty": 1.0,
|
||||
"qty": 3.0,
|
||||
"rate": 100.0,
|
||||
"stock_uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
@@ -914,12 +914,6 @@ def update_item_details(child_row, details):
|
||||
else child_row.get("consumed_qty")
|
||||
)
|
||||
|
||||
if child_row.serial_no:
|
||||
details.serial_no.extend(get_serial_nos(child_row.serial_no))
|
||||
|
||||
if child_row.batch_no:
|
||||
details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
|
||||
|
||||
if child_row.serial_and_batch_bundle:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
|
||||
for row in doc.get("entries"):
|
||||
@@ -928,6 +922,12 @@ def update_item_details(child_row, details):
|
||||
|
||||
if row.batch_no:
|
||||
details.batch_no[row.batch_no] += row.qty * (-1 if doc.type_of_transaction == "Outward" else 1)
|
||||
else:
|
||||
if child_row.serial_no:
|
||||
details.serial_no.extend(get_serial_nos(child_row.serial_no))
|
||||
|
||||
if child_row.batch_no:
|
||||
details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
|
||||
|
||||
|
||||
def make_stock_transfer_entry(**args):
|
||||
|
||||
@@ -41,7 +41,9 @@ class SalesPipelineAnalytics(object):
|
||||
month_list = self.get_month_list()
|
||||
|
||||
for month in month_list:
|
||||
self.columns.append({"fieldname": month, "fieldtype": based_on, "label": month, "width": 200})
|
||||
self.columns.append(
|
||||
{"fieldname": month, "fieldtype": based_on, "label": _(month), "width": 200}
|
||||
)
|
||||
|
||||
elif self.filters.get("range") == "Quarterly":
|
||||
for quarter in range(1, 5):
|
||||
@@ -156,7 +158,7 @@ class SalesPipelineAnalytics(object):
|
||||
|
||||
for column in self.columns:
|
||||
if column["fieldname"] != "opportunity_owner" and column["fieldname"] != "sales_stage":
|
||||
labels.append(column["fieldname"])
|
||||
labels.append(_(column["fieldname"]))
|
||||
|
||||
self.chart = {"data": {"labels": labels, "datasets": datasets}, "type": "line"}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.model.document import Document
|
||||
from frappe.utils import add_months, formatdate, getdate, sbool, today
|
||||
from plaid.errors import ItemError
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
|
||||
|
||||
|
||||
@@ -90,9 +89,15 @@ def add_bank_accounts(response, bank, company):
|
||||
bank = json.loads(bank)
|
||||
result = []
|
||||
|
||||
default_gl_account = get_default_bank_cash_account(company, "Bank")
|
||||
if not default_gl_account:
|
||||
frappe.throw(_("Please setup a default bank account for company {0}").format(company))
|
||||
parent_gl_account = frappe.db.get_all(
|
||||
"Account", {"company": company, "account_type": "Bank", "is_group": 1, "disabled": 0}
|
||||
)
|
||||
if not parent_gl_account:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please setup and enable a group account with the Account Type - {0} for the company {1}"
|
||||
).format(frappe.bold("Bank"), company)
|
||||
)
|
||||
|
||||
for account in response["accounts"]:
|
||||
acc_type = frappe.db.get_value("Bank Account Type", account["type"])
|
||||
@@ -108,11 +113,22 @@ def add_bank_accounts(response, bank, company):
|
||||
|
||||
if not existing_bank_account:
|
||||
try:
|
||||
gl_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": account["name"] + " - " + response["institution"]["name"],
|
||||
"parent_account": parent_gl_account[0].name,
|
||||
"account_type": "Bank",
|
||||
"company": company,
|
||||
}
|
||||
)
|
||||
gl_account.insert(ignore_if_duplicate=True)
|
||||
|
||||
new_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"bank": bank["bank_name"],
|
||||
"account": default_gl_account.account,
|
||||
"account": gl_account.name,
|
||||
"account_name": account["name"],
|
||||
"account_type": account.get("type", ""),
|
||||
"account_subtype": account.get("subtype", ""),
|
||||
|
||||
@@ -7,7 +7,6 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils.response import json_handler
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
|
||||
add_account_subtype,
|
||||
add_account_type,
|
||||
@@ -72,14 +71,6 @@ class TestPlaidSettings(unittest.TestCase):
|
||||
bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
|
||||
company = frappe.db.get_single_value("Global Defaults", "default_company")
|
||||
|
||||
if frappe.db.get_value("Company", company, "default_bank_account") is None:
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
"default_bank_account",
|
||||
get_default_bank_cash_account(company, "Cash").get("account"),
|
||||
)
|
||||
|
||||
add_bank_accounts(bank_accounts, bank, company)
|
||||
|
||||
transactions = {
|
||||
|
||||
@@ -18,7 +18,7 @@ frappe.ui.form.on('Blanket Order', {
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
erpnext.hide_company();
|
||||
erpnext.hide_company(frm);
|
||||
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
|
||||
frm.add_custom_button(__("Sales Order"), function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
|
||||
@@ -41,11 +41,49 @@ class BlanketOrder(Document):
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_duplicate_items()
|
||||
self.set_party_item_code()
|
||||
|
||||
def validate_dates(self):
|
||||
if getdate(self.from_date) > getdate(self.to_date):
|
||||
frappe.throw(_("From date cannot be greater than To date"))
|
||||
|
||||
def set_party_item_code(self):
|
||||
item_ref = {}
|
||||
if self.blanket_order_type == "Selling":
|
||||
item_ref = self.get_customer_items_ref()
|
||||
else:
|
||||
item_ref = self.get_supplier_items_ref()
|
||||
|
||||
if not item_ref:
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
row.party_item_code = item_ref.get(row.item_code)
|
||||
|
||||
def get_customer_items_ref(self):
|
||||
items = [d.item_code for d in self.items]
|
||||
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item Customer Detail",
|
||||
filters={"parent": ("in", items), "customer_name": self.customer},
|
||||
fields=["parent", "ref_code"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
def get_supplier_items_ref(self):
|
||||
items = [d.item_code for d in self.items]
|
||||
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item Supplier",
|
||||
filters={"parent": ("in", items), "supplier": self.supplier},
|
||||
fields=["parent", "supplier_part_no"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
def validate_duplicate_items(self):
|
||||
item_list = []
|
||||
for item in self.items:
|
||||
|
||||
@@ -5,6 +5,7 @@ from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
from .blanket_order import make_order
|
||||
|
||||
@@ -90,6 +91,30 @@ class TestBlanketOrder(FrappeTestCase):
|
||||
frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10)
|
||||
po.submit()
|
||||
|
||||
def test_party_item_code(self):
|
||||
item_doc = make_item("_Test Item 1 for Blanket Order")
|
||||
item_code = item_doc.name
|
||||
|
||||
customer = "_Test Customer"
|
||||
supplier = "_Test Supplier"
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Item Customer Detail", {"customer_name": customer, "parent": item_code}
|
||||
):
|
||||
item_doc.append("customer_items", {"customer_name": customer, "ref_code": "CUST-REF-1"})
|
||||
item_doc.save()
|
||||
|
||||
if not frappe.db.exists("Item Supplier", {"supplier": supplier, "parent": item_code}):
|
||||
item_doc.append("supplier_items", {"supplier": supplier, "supplier_part_no": "SUPP-PART-1"})
|
||||
item_doc.save()
|
||||
|
||||
# Blanket Order for Selling
|
||||
bo = make_blanket_order(blanket_order_type="Selling", customer=customer, item_code=item_code)
|
||||
self.assertEqual(bo.items[0].party_item_code, "CUST-REF-1")
|
||||
|
||||
bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code)
|
||||
self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1")
|
||||
|
||||
|
||||
def make_blanket_order(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2018-05-24 07:20:04.255236",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -6,6 +7,7 @@
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"party_item_code",
|
||||
"column_break_3",
|
||||
"qty",
|
||||
"rate",
|
||||
@@ -62,10 +64,17 @@
|
||||
"fieldname": "terms_and_conditions",
|
||||
"fieldtype": "Text",
|
||||
"label": "Terms and Conditions"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_item_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Item Code",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-11-18 19:37:46.245878",
|
||||
"links": [],
|
||||
"modified": "2024-02-14 18:25:26.479672",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order Item",
|
||||
@@ -74,5 +83,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class BlanketOrderItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party_item_code: DF.Data | None
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
terms_and_conditions: DF.Text | None
|
||||
|
||||
@@ -1071,8 +1071,7 @@ def get_valuation_rate(data):
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.valuation_rate)
|
||||
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
|
||||
.orderby(sle.posting_date, order=frappe.qb.desc)
|
||||
.orderby(sle.posting_time, order=frappe.qb.desc)
|
||||
.orderby(sle.posting_datetime, order=frappe.qb.desc)
|
||||
.orderby(sle.creation, order=frappe.qb.desc)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -239,12 +239,12 @@ class JobCard(Document):
|
||||
for row in self.sub_operations:
|
||||
self.total_completed_qty += row.completed_qty
|
||||
|
||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||
def get_overlap_for(self, args):
|
||||
time_logs = []
|
||||
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log", check_next_available_slot))
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
||||
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", check_next_available_slot))
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
|
||||
|
||||
if not time_logs:
|
||||
return {}
|
||||
@@ -269,7 +269,7 @@ class JobCard(Document):
|
||||
self.workstation = workstation_time.get("workstation")
|
||||
return workstation_time
|
||||
|
||||
return time_logs[-1]
|
||||
return time_logs[0]
|
||||
|
||||
def has_overlap(self, production_capacity, time_logs):
|
||||
overlap = False
|
||||
@@ -308,7 +308,7 @@ class JobCard(Document):
|
||||
return True
|
||||
return overlap
|
||||
|
||||
def get_time_logs(self, args, doctype, check_next_available_slot=False):
|
||||
def get_time_logs(self, args, doctype):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType(doctype)
|
||||
|
||||
@@ -318,9 +318,6 @@ class JobCard(Document):
|
||||
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
|
||||
]
|
||||
|
||||
if check_next_available_slot:
|
||||
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(jctl)
|
||||
.from_(jc)
|
||||
@@ -395,18 +392,28 @@ class JobCard(Document):
|
||||
|
||||
def validate_overlap_for_workstation(self, args, row):
|
||||
# get the last record based on the to time from the job card
|
||||
data = self.get_overlap_for(args, check_next_available_slot=True)
|
||||
data = self.get_overlap_for(args)
|
||||
|
||||
if not self.workstation:
|
||||
workstations = get_workstations(self.workstation_type)
|
||||
if workstations:
|
||||
# Get the first workstation
|
||||
self.workstation = workstations[0]
|
||||
|
||||
if not data:
|
||||
row.planned_start_time = args.from_time
|
||||
return
|
||||
|
||||
if data:
|
||||
if data.get("planned_start_time"):
|
||||
row.planned_start_time = get_datetime(data.planned_start_time)
|
||||
args.planned_start_time = get_datetime(data.planned_start_time)
|
||||
else:
|
||||
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
|
||||
args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
|
||||
|
||||
args.from_time = args.planned_start_time
|
||||
args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins)
|
||||
|
||||
self.validate_overlap_for_workstation(args, row)
|
||||
|
||||
def check_workstation_time(self, row):
|
||||
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"field_order": [
|
||||
"raw_materials_consumption_section",
|
||||
"material_consumption",
|
||||
"get_rm_cost_from_consumption_entry",
|
||||
"column_break_3",
|
||||
"backflush_raw_materials_based_on",
|
||||
"capacity_planning",
|
||||
@@ -202,13 +203,20 @@
|
||||
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Operating Cost / Scrape Items From Sub-assemblies"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.material_consumption",
|
||||
"fieldname": "get_rm_cost_from_consumption_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Get Raw Materials Cost from Consumption Entry"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-28 16:37:44.874096",
|
||||
"modified": "2024-02-08 19:00:37.561244",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
||||
@@ -26,6 +26,7 @@ class ManufacturingSettings(Document):
|
||||
default_scrap_warehouse: DF.Link | None
|
||||
default_wip_warehouse: DF.Link | None
|
||||
disable_capacity_planning: DF.Check
|
||||
get_rm_cost_from_consumption_entry: DF.Check
|
||||
job_card_excess_transfer: DF.Check
|
||||
make_serial_no_batch_from_work_order: DF.Check
|
||||
material_consumption: DF.Check
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
@@ -53,7 +54,8 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "For Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
@@ -141,7 +143,8 @@
|
||||
"fieldname": "from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "From Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.safety_stock",
|
||||
@@ -199,7 +202,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-12 12:09:08.358326",
|
||||
"modified": "2024-02-11 16:21:11.977018",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Material Request Plan Item",
|
||||
|
||||
@@ -518,6 +518,12 @@ frappe.ui.form.on("Production Plan Sales Order", {
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Production Plan Sub Assembly Item", {
|
||||
fg_warehouse(frm, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
|
||||
},
|
||||
})
|
||||
|
||||
frappe.tour['Production Plan'] = [
|
||||
{
|
||||
fieldname: "get_items_from",
|
||||
|
||||
@@ -298,7 +298,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@@ -420,9 +421,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",
|
||||
"fieldname": "sub_assembly_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sub Assembly Warehouse",
|
||||
"mandatory_depends_on": "eval:doc.skip_available_sub_assembly_item === 1",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -436,7 +439,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-26 16:31:13.740777",
|
||||
"modified": "2024-02-27 13:34:20.692211",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -312,9 +312,10 @@ class ProductionPlan(Document):
|
||||
so_item.parent,
|
||||
so_item.item_code,
|
||||
so_item.warehouse,
|
||||
(
|
||||
(so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
|
||||
).as_("pending_qty"),
|
||||
so_item.qty,
|
||||
so_item.work_order_qty,
|
||||
so_item.delivered_qty,
|
||||
so_item.conversion_factor,
|
||||
so_item.description,
|
||||
so_item.name,
|
||||
so_item.bom_no,
|
||||
@@ -337,6 +338,11 @@ class ProductionPlan(Document):
|
||||
|
||||
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")
|
||||
|
||||
packed_items_query = (
|
||||
@@ -646,7 +652,10 @@ class ProductionPlan(Document):
|
||||
"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:
|
||||
key = (d.name, d.item_code, d.warehouse)
|
||||
|
||||
@@ -885,8 +894,8 @@ class ProductionPlan(Document):
|
||||
sub_assembly_items_store = [] # temporary store to process all subassembly items
|
||||
|
||||
for row in self.po_items:
|
||||
if self.skip_available_sub_assembly_item and not row.warehouse:
|
||||
frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
|
||||
if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse:
|
||||
frappe.throw(_("Row #{0}: Please select the Sub Assembly Warehouse").format(row.idx))
|
||||
|
||||
if not row.item_code:
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
@@ -896,15 +905,24 @@ class ProductionPlan(Document):
|
||||
|
||||
bom_data = []
|
||||
|
||||
warehouse = (
|
||||
(self.sub_assembly_warehouse or row.warehouse)
|
||||
if self.skip_available_sub_assembly_item
|
||||
else None
|
||||
)
|
||||
warehouse = (self.sub_assembly_warehouse) if self.skip_available_sub_assembly_item else None
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
sub_assembly_items_store.extend(bom_data)
|
||||
|
||||
if not sub_assembly_items_store and self.skip_available_sub_assembly_item:
|
||||
message = (
|
||||
_(
|
||||
"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}."
|
||||
).format(self.sub_assembly_warehouse)
|
||||
+ "<br><br>"
|
||||
)
|
||||
message += _(
|
||||
"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox."
|
||||
)
|
||||
|
||||
frappe.msgprint(message, title=_("Note"))
|
||||
|
||||
if self.combine_sub_items:
|
||||
# Combine subassembly items
|
||||
sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
|
||||
@@ -917,15 +935,19 @@ class ProductionPlan(Document):
|
||||
|
||||
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||
"Modify bom_data, set additional details."
|
||||
is_group_warehouse = frappe.db.get_value("Warehouse", self.sub_assembly_warehouse, "is_group")
|
||||
|
||||
for data in bom_data:
|
||||
data.qty = data.stock_qty
|
||||
data.production_plan_item = row.name
|
||||
data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
|
||||
data.schedule_date = row.planned_start_date
|
||||
data.type_of_manufacturing = manufacturing_type or (
|
||||
"Subcontract" if data.is_sub_contracted_item else "In House"
|
||||
)
|
||||
|
||||
if not is_group_warehouse:
|
||||
data.fg_warehouse = self.sub_assembly_warehouse
|
||||
|
||||
def set_default_supplier_for_subcontracting_order(self):
|
||||
items = [
|
||||
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"
|
||||
@@ -1469,7 +1491,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
so_item_details = frappe._dict()
|
||||
|
||||
sub_assembly_items = {}
|
||||
if doc.get("skip_available_sub_assembly_item"):
|
||||
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
|
||||
for d in doc.get("sub_assembly_items"):
|
||||
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
|
||||
|
||||
@@ -1498,19 +1520,17 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
|
||||
|
||||
if bom_no:
|
||||
if (
|
||||
data.get("include_exploded_items")
|
||||
and doc.get("sub_assembly_items")
|
||||
and doc.get("skip_available_sub_assembly_item")
|
||||
):
|
||||
item_details = get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"):
|
||||
item_details = {}
|
||||
if doc.get("sub_assembly_items"):
|
||||
item_details = get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
|
||||
elif data.get("include_exploded_items") and include_subcontracted_items:
|
||||
# fetch exploded items from BOM
|
||||
@@ -1683,34 +1703,37 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
|
||||
if warehouse:
|
||||
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
|
||||
if bin_dict and bin_dict[0].projected_qty > 0:
|
||||
if bin_dict[0].projected_qty > stock_qty:
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - bin_dict[0].projected_qty
|
||||
for _bin_dict in bin_details:
|
||||
if _bin_dict.projected_qty > 0:
|
||||
if _bin_dict.projected_qty > stock_qty:
|
||||
stock_qty = 0
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - _bin_dict.projected_qty
|
||||
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"parent_item_code": parent_item_code,
|
||||
"description": d.description,
|
||||
"production_item": d.item_code,
|
||||
"item_name": d.item_name,
|
||||
"stock_uom": d.stock_uom,
|
||||
"uom": d.stock_uom,
|
||||
"bom_no": d.value,
|
||||
"is_sub_contracted_item": d.is_sub_contracted_item,
|
||||
"bom_level": indent,
|
||||
"indent": indent,
|
||||
"stock_qty": stock_qty,
|
||||
}
|
||||
if stock_qty > 0:
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"parent_item_code": parent_item_code,
|
||||
"description": d.description,
|
||||
"production_item": d.item_code,
|
||||
"item_name": d.item_name,
|
||||
"stock_uom": d.stock_uom,
|
||||
"uom": d.stock_uom,
|
||||
"bom_no": d.value,
|
||||
"is_sub_contracted_item": d.is_sub_contracted_item,
|
||||
"bom_level": indent,
|
||||
"indent": indent,
|
||||
"stock_qty": stock_qty,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
|
||||
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
@@ -1762,23 +1785,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_non_completed_production_plans():
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Production Plan Item")
|
||||
|
||||
query = (
|
||||
return (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(table.name)
|
||||
.distinct()
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
& (child.planned_qty > child.ordered_qty)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return list(set([d.name for d in query]))
|
||||
).run(pluck="name")
|
||||
|
||||
|
||||
def get_raw_materials_of_sub_assembly_items(
|
||||
|
||||
@@ -1205,6 +1205,7 @@ class TestProductionPlan(FrappeTestCase):
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
sub_assembly_warehouse="_Test Warehouse - _TC",
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
@@ -1232,6 +1233,35 @@ class TestProductionPlan(FrappeTestCase):
|
||||
if row.item_code == "SubAssembly2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 10)
|
||||
|
||||
def test_sub_assembly_and_their_raw_materials_exists(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
bom_tree = {
|
||||
"FG1 For SUB Test": {
|
||||
"SAB1 For SUB Test": {"CP1 For SUB Test": {}},
|
||||
"SAB2 For SUB Test": {},
|
||||
}
|
||||
}
|
||||
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
for item in ["SAB1 For SUB Test", "SAB2 For SUB Test"]:
|
||||
make_stock_entry(item_code=item, qty=10, rate=100, target="_Test Warehouse - _TC")
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=10,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
items = get_items_for_material_requests(
|
||||
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
|
||||
)
|
||||
|
||||
self.assertFalse(items)
|
||||
|
||||
def test_transfer_and_purchase_mrp_for_purchase_uom(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
@@ -1309,6 +1339,7 @@ class TestProductionPlan(FrappeTestCase):
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
sub_assembly_warehouse="_Test Warehouse - _TC",
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
@@ -1561,6 +1592,48 @@ class TestProductionPlan(FrappeTestCase):
|
||||
for row in work_orders:
|
||||
self.assertEqual(row.qty, wo_qty[row.name])
|
||||
|
||||
def test_parent_warehouse_for_sub_assembly_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
parent_warehouse = "_Test Warehouse Group - _TC"
|
||||
sub_warehouse = create_warehouse("Sub Warehouse", company="_Test Company")
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
sf_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom_tree = {fg_item: {sf_item: {rm_item: {}}}}
|
||||
create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code=fg_item,
|
||||
planned_qty=10,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
sub_assembly_warehouse=parent_warehouse,
|
||||
skip_available_sub_assembly_item=1,
|
||||
do_not_submit=1,
|
||||
skip_getting_mr_items=1,
|
||||
)
|
||||
|
||||
pln.get_sub_assembly_items()
|
||||
|
||||
for row in pln.sub_assembly_items:
|
||||
self.assertFalse(row.fg_warehouse)
|
||||
self.assertEqual(row.production_item, sf_item)
|
||||
self.assertEqual(row.qty, 10.0)
|
||||
|
||||
make_stock_entry(item_code=sf_item, qty=5, target=sub_warehouse, rate=100)
|
||||
|
||||
pln.sub_assembly_items = []
|
||||
pln.get_sub_assembly_items()
|
||||
|
||||
self.assertEqual(pln.sub_assembly_warehouse, parent_warehouse)
|
||||
for row in pln.sub_assembly_items:
|
||||
self.assertFalse(row.fg_warehouse)
|
||||
self.assertEqual(row.production_item, sf_item)
|
||||
self.assertEqual(row.qty, 5.0)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"bom_no",
|
||||
"column_break_6",
|
||||
"planned_qty",
|
||||
"stock_uom",
|
||||
"warehouse",
|
||||
"planned_start_date",
|
||||
"section_break_9",
|
||||
@@ -18,7 +19,6 @@
|
||||
"ordered_qty",
|
||||
"column_break_17",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"produced_qty",
|
||||
"reference_section",
|
||||
"sales_order",
|
||||
@@ -65,6 +65,7 @@
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "planned_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
@@ -80,6 +81,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -141,8 +143,10 @@
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "UOM",
|
||||
"oldfieldname": "stock_uom",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -216,7 +220,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-25 14:15:40.061514",
|
||||
"modified": "2024-02-27 13:24:43.571844",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
"columns": 1,
|
||||
"fieldname": "bom_level",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Level (BOM)",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -149,8 +148,10 @@
|
||||
"label": "Indent"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "fg_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -170,6 +171,7 @@
|
||||
"options": "Supplier"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
@@ -207,7 +209,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 13:33:42.959387",
|
||||
"modified": "2024-02-27 13:45:17.422435",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
||||
@@ -1775,6 +1775,159 @@ class TestWorkOrder(FrappeTestCase):
|
||||
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
|
||||
)
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
|
||||
)
|
||||
def test_get_rm_cost_from_consumption_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
rm = make_item(properties={"is_stock_item": 1}).name
|
||||
fg = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
make_stock_entry_test_record(
|
||||
purpose="Material Receipt",
|
||||
item_code=rm,
|
||||
target="Stores - _TC",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
make_stock_entry_test_record(
|
||||
purpose="Material Receipt",
|
||||
item_code=rm,
|
||||
target="Stores - _TC",
|
||||
qty=10,
|
||||
basic_rate=200,
|
||||
)
|
||||
|
||||
bom = make_bom(item=fg, raw_materials=[rm], rate=150).name
|
||||
wo = make_wo_order_test_record(
|
||||
production_item=fg,
|
||||
bom_no=bom,
|
||||
qty=10,
|
||||
)
|
||||
|
||||
mte = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
|
||||
mte.items[0].s_warehouse = "Stores - _TC"
|
||||
mte.insert().submit()
|
||||
|
||||
mce = frappe.get_doc(make_stock_entry(wo.name, "Material Consumption for Manufacture", 10))
|
||||
mce.insert().submit()
|
||||
|
||||
me = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
me.insert().submit()
|
||||
|
||||
valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
|
||||
self.assertEqual(me.items[0].valuation_rate, valuation_rate)
|
||||
|
||||
def test_capcity_planning_for_workstation(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
{
|
||||
"disable_capacity_planning": 0,
|
||||
"capacity_planning_for_days": 1,
|
||||
"mins_between_operations": 10,
|
||||
},
|
||||
)
|
||||
|
||||
properties = {"is_stock_item": 1, "valuation_rate": 100}
|
||||
fg_item = make_item("Test FG Item For Capacity Planning", properties).name
|
||||
|
||||
rm_item = make_item("Test RM Item For Capacity Planning", properties).name
|
||||
|
||||
workstation = "Test Workstation For Capacity Planning"
|
||||
if not frappe.db.exists("Workstation", workstation):
|
||||
make_workstation(workstation=workstation, production_capacity=1)
|
||||
|
||||
operation = "Test Operation For Capacity Planning"
|
||||
if not frappe.db.exists("Operation", operation):
|
||||
make_operation(operation=operation, workstation=workstation)
|
||||
|
||||
bom_doc = make_bom(
|
||||
item=fg_item,
|
||||
source_warehouse="Stores - _TC",
|
||||
raw_materials=[rm_item],
|
||||
with_operations=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
bom_doc.append(
|
||||
"operations",
|
||||
{"operation": operation, "time_in_mins": 1420, "hour_rate": 100, "workstation": workstation},
|
||||
)
|
||||
bom_doc.submit()
|
||||
|
||||
# 1st Work Order,
|
||||
# Capacity to run parallel the operation 'Test Operation For Capacity Planning' is 2
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
|
||||
)
|
||||
|
||||
wo_doc.submit()
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name},
|
||||
)
|
||||
|
||||
self.assertEqual(len(job_cards), 1)
|
||||
|
||||
# 2nd Work Order,
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
|
||||
)
|
||||
|
||||
wo_doc.submit()
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name},
|
||||
)
|
||||
|
||||
self.assertEqual(len(job_cards), 1)
|
||||
|
||||
# 3rd Work Order, capacity is full
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
|
||||
)
|
||||
|
||||
self.assertRaises(CapacityError, wo_doc.submit)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0}
|
||||
)
|
||||
|
||||
|
||||
def make_operation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
operation_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Operation",
|
||||
"name": kwargs.operation,
|
||||
"workstation": kwargs.workstation,
|
||||
}
|
||||
)
|
||||
operation_doc.insert()
|
||||
|
||||
return operation_doc
|
||||
|
||||
|
||||
def make_workstation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
workstation_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Workstation",
|
||||
"workstation_name": kwargs.workstation,
|
||||
"workstation_type": kwargs.workstation_type,
|
||||
"production_capacity": kwargs.production_capacity or 0,
|
||||
"hour_rate": kwargs.hour_rate or 100,
|
||||
}
|
||||
)
|
||||
workstation_doc.insert()
|
||||
|
||||
return workstation_doc
|
||||
|
||||
|
||||
def prepare_boms_for_sub_assembly_test():
|
||||
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
|
||||
|
||||
@@ -447,7 +447,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_item",
|
||||
@@ -592,7 +593,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-11 18:35:49.852069",
|
||||
"modified": "2024-02-11 15:47:13.454422",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -242,8 +242,12 @@ class WorkOrder(Document):
|
||||
def calculate_operating_cost(self):
|
||||
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
|
||||
for d in self.get("operations"):
|
||||
d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0)
|
||||
d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0)
|
||||
d.planned_operating_cost = flt(
|
||||
flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost")
|
||||
)
|
||||
d.actual_operating_cost = flt(
|
||||
flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0), d.precision("actual_operating_cost")
|
||||
)
|
||||
|
||||
self.planned_operating_cost += flt(d.planned_operating_cost)
|
||||
self.actual_operating_cost += flt(d.actual_operating_cost)
|
||||
@@ -588,7 +592,6 @@ class WorkOrder(Document):
|
||||
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
|
||||
self.set_operation_start_end_time(index, row)
|
||||
|
||||
original_start_time = row.planned_start_time
|
||||
job_card_doc = create_job_card(
|
||||
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
|
||||
)
|
||||
@@ -597,11 +600,15 @@ class WorkOrder(Document):
|
||||
row.planned_start_time = job_card_doc.scheduled_time_logs[-1].from_time
|
||||
row.planned_end_time = job_card_doc.scheduled_time_logs[-1].to_time
|
||||
|
||||
if date_diff(row.planned_start_time, original_start_time) > plan_days:
|
||||
if date_diff(row.planned_end_time, self.planned_start_date) > plan_days:
|
||||
frappe.message_log.pop()
|
||||
frappe.throw(
|
||||
_("Unable to find the time slot in the next {0} days for the operation {1}.").format(
|
||||
plan_days, row.operation
|
||||
_(
|
||||
"Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}."
|
||||
).format(
|
||||
plan_days,
|
||||
row.operation,
|
||||
get_link_to_form("Manufacturing Settings", "Manufacturing Settings"),
|
||||
),
|
||||
CapacityError,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item"
|
||||
"options": "Item",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "source_warehouse",
|
||||
@@ -141,7 +142,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-28 10:50:43.512562",
|
||||
"modified": "2024-02-11 15:45:32.318374",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Item",
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-08-12 12:44:27",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-13 04:58:51.549413",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Completed Work Orders",
|
||||
"owner": "Administrator",
|
||||
"query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) = `tabWork Order`.qty",
|
||||
"ref_doctype": "Work Order",
|
||||
"report_name": "Completed Work Orders",
|
||||
"report_type": "Query Report",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-08-12 12:44:27",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-02-21 14:35:14.301848",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Completed Work Orders",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) >= `tabWork Order`.qty",
|
||||
"ref_doctype": "Work Order",
|
||||
"report_name": "Completed Work Orders",
|
||||
"report_type": "Query Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Manufacturing User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_data(filters):
|
||||
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
|
||||
|
||||
data = frappe.get_all(
|
||||
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
|
||||
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
|
||||
)
|
||||
|
||||
res = []
|
||||
|
||||
@@ -264,6 +264,7 @@ execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Deta
|
||||
|
||||
[post_model_sync]
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024
|
||||
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||
@@ -351,7 +352,9 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
|
||||
erpnext.patches.v14_0.update_zero_asset_quantity_field
|
||||
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
@@ -0,0 +1,8 @@
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
create_accounting_dimensions_for_doctype,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation")
|
||||
create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation Allocation")
|
||||
@@ -0,0 +1,19 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabStock Ledger Entry`
|
||||
SET posting_datetime = DATE_FORMAT(timestamp(posting_date, posting_time), '%Y-%m-%d %H:%i:%s')
|
||||
"""
|
||||
)
|
||||
|
||||
drop_indexes()
|
||||
|
||||
|
||||
def drop_indexes():
|
||||
if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
|
||||
return
|
||||
|
||||
frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`")
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
# nosemgrep
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabAsset Movement Item`
|
||||
WHERE parent NOT IN (SELECT name FROM `tabAsset Movement`)
|
||||
"""
|
||||
)
|
||||
@@ -35,6 +35,14 @@ frappe.ui.form.on("Project", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("department", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// sales order
|
||||
frm.set_query('sales_order', function () {
|
||||
var filters = {
|
||||
|
||||
@@ -83,7 +83,7 @@ class Timesheet(Document):
|
||||
def set_status(self):
|
||||
self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)]
|
||||
|
||||
if self.per_billed == 100:
|
||||
if flt(self.per_billed, self.precision("per_billed")) >= 100.0:
|
||||
self.status = "Billed"
|
||||
|
||||
if self.sales_invoice:
|
||||
|
||||
@@ -368,7 +368,8 @@ erpnext.buying = {
|
||||
|
||||
let update_values = {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": qty
|
||||
"use_serial_batch_fields": 0,
|
||||
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
|
||||
}
|
||||
|
||||
if (r.warehouse) {
|
||||
@@ -408,7 +409,8 @@ erpnext.buying = {
|
||||
|
||||
let update_values = {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"rejected_qty": qty
|
||||
"use_serial_batch_fields": 0,
|
||||
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
|
||||
}
|
||||
|
||||
if (r.warehouse) {
|
||||
|
||||
@@ -106,7 +106,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
frappe.ui.form.on(this.frm.doctype + " Item", {
|
||||
items_add: function(frm, cdt, cdn) {
|
||||
debugger
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if (!item.warehouse && frm.doc.set_warehouse) {
|
||||
item.warehouse = frm.doc.set_warehouse;
|
||||
@@ -145,6 +144,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict['items'].grid.get_field('batch_no')) {
|
||||
this.frm.set_query('batch_no', 'items', function(doc, cdt, cdn) {
|
||||
return me.set_query_for_batch(doc, cdt, cdn);
|
||||
});
|
||||
}
|
||||
|
||||
if(
|
||||
this.frm.docstatus < 2
|
||||
&& this.frm.fields_dict["payment_terms_template"]
|
||||
@@ -160,7 +165,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["items"]) {
|
||||
this["items_remove"] = this.calculate_net_weight;
|
||||
this["items_remove"] = this.process_item_removal;
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["recurring_print_format"]) {
|
||||
@@ -234,7 +239,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
set_fields_onload_for_line_item() {
|
||||
if (this.frm.is_new && this.frm.doc?.items) {
|
||||
if (this.frm.is_new() && this.frm.doc?.items) {
|
||||
this.frm.doc.items.forEach(item => {
|
||||
if (item.docstatus === 0
|
||||
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
|
||||
@@ -392,7 +397,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
refresh() {
|
||||
erpnext.toggle_naming_series();
|
||||
erpnext.hide_company();
|
||||
erpnext.hide_company(this.frm);
|
||||
this.set_dynamic_labels();
|
||||
this.setup_sms();
|
||||
this.setup_quality_inspection();
|
||||
@@ -1158,6 +1163,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
me.apply_price_list(item, true)
|
||||
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
|
||||
}
|
||||
}
|
||||
@@ -1282,6 +1288,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
process_item_removal() {
|
||||
this.frm.trigger("calculate_taxes_and_totals");
|
||||
this.frm.trigger("calculate_net_weight");
|
||||
}
|
||||
|
||||
calculate_net_weight(){
|
||||
/* Calculate Total Net Weight then further applied shipping rule to calculate shipping charges.*/
|
||||
var me = this;
|
||||
@@ -1503,31 +1514,35 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
remove_pricing_rule_for_item(item) {
|
||||
let me = this;
|
||||
return this.frm.call({
|
||||
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
|
||||
args: {
|
||||
pricing_rules: item.pricing_rules,
|
||||
item_details: {
|
||||
"doctype": item.doctype,
|
||||
"name": item.name,
|
||||
"item_code": item.item_code,
|
||||
"pricing_rules": item.pricing_rules,
|
||||
"parenttype": item.parenttype,
|
||||
"parent": item.parent,
|
||||
"price_list_rate": item.price_list_rate
|
||||
// capture pricing rule before removing it to delete free items
|
||||
let removed_pricing_rule = item.pricing_rules;
|
||||
if (item.pricing_rules){
|
||||
let me = this;
|
||||
return this.frm.call({
|
||||
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
|
||||
args: {
|
||||
pricing_rules: item.pricing_rules,
|
||||
item_details: {
|
||||
"doctype": item.doctype,
|
||||
"name": item.name,
|
||||
"item_code": item.item_code,
|
||||
"pricing_rules": item.pricing_rules,
|
||||
"parenttype": item.parenttype,
|
||||
"parent": item.parent,
|
||||
"price_list_rate": item.price_list_rate
|
||||
},
|
||||
item_code: item.item_code,
|
||||
rate: item.price_list_rate,
|
||||
},
|
||||
item_code: item.item_code,
|
||||
rate: item.price_list_rate,
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message) {
|
||||
me.remove_pricing_rule(r.message);
|
||||
me.calculate_taxes_and_totals();
|
||||
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message) {
|
||||
me.remove_pricing_rule(r.message, removed_pricing_rule);
|
||||
me.calculate_taxes_and_totals();
|
||||
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
apply_pricing_rule(item, calculate_taxes_and_totals) {
|
||||
@@ -1633,18 +1648,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
return item_list;
|
||||
}
|
||||
|
||||
items_delete() {
|
||||
this.update_localstorage_scanned_data();
|
||||
}
|
||||
|
||||
update_localstorage_scanned_data() {
|
||||
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
|
||||
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||
barcode_scanner.update_localstorage_scanned_data();
|
||||
}
|
||||
}
|
||||
|
||||
_set_values_for_item_list(children) {
|
||||
const items_rule_dict = {};
|
||||
|
||||
@@ -1791,7 +1794,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
});
|
||||
}
|
||||
|
||||
remove_pricing_rule(item) {
|
||||
remove_pricing_rule(item, removed_pricing_rule) {
|
||||
let me = this;
|
||||
const fields = ["discount_percentage",
|
||||
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
|
||||
@@ -1800,7 +1803,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let items = [];
|
||||
|
||||
me.frm.doc.items.forEach(d => {
|
||||
if(d.item_code != item.remove_free_item || !d.is_free_item) {
|
||||
// if same item was added a free item through a different pricing rule, keep it
|
||||
if(d.item_code != item.remove_free_item || !d.is_free_item || removed_pricing_rule?.includes(d.pricing_rules)) {
|
||||
items.push(d);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,21 +26,24 @@ $.extend(erpnext, {
|
||||
}
|
||||
},
|
||||
|
||||
hide_company: function() {
|
||||
if(cur_frm.fields_dict.company) {
|
||||
hide_company: function(frm) {
|
||||
if(frm?.fields_dict.company) {
|
||||
var companies = Object.keys(locals[":Company"] || {});
|
||||
if(companies.length === 1) {
|
||||
if(!cur_frm.doc.company) cur_frm.set_value("company", companies[0]);
|
||||
cur_frm.toggle_display("company", false);
|
||||
if(!frm.doc.company) frm.set_value("company", companies[0]);
|
||||
frm.toggle_display("company", false);
|
||||
} else if(erpnext.last_selected_company) {
|
||||
if(!cur_frm.doc.company) cur_frm.set_value("company", erpnext.last_selected_company);
|
||||
if(!frm.doc.company) frm.set_value("company", erpnext.last_selected_company);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
is_perpetual_inventory_enabled: function(company) {
|
||||
if(company) {
|
||||
return frappe.get_doc(":Company", company).enable_perpetual_inventory
|
||||
let company_local = locals[":Company"] && locals[":Company"][company];
|
||||
if(company_local) {
|
||||
return cint(company_local.enable_perpetual_inventory);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -339,7 +339,8 @@ erpnext.sales_common = {
|
||||
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": qty
|
||||
"use_serial_batch_fields": 0,
|
||||
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
let warehouse = this.item?.type_of_transaction === "Outward" ?
|
||||
(this.item.warehouse || this.item.s_warehouse) : "";
|
||||
|
||||
if (this.frm.doc.doctype === 'Stock Entry') {
|
||||
warehouse = this.item.s_warehouse || this.item.t_warehouse;
|
||||
}
|
||||
|
||||
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
|
||||
warehouse = this.get_warehouse();
|
||||
}
|
||||
@@ -367,19 +371,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
label: __('Batch No'),
|
||||
in_list_view: 1,
|
||||
get_query: () => {
|
||||
if (this.item.type_of_transaction !== "Outward") {
|
||||
return {
|
||||
filters: {
|
||||
'item': this.item.item_code,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
query : "erpnext.controllers.queries.get_batch_no",
|
||||
filters: {
|
||||
'item_code': this.item.item_code,
|
||||
'warehouse': this.get_warehouse()
|
||||
}
|
||||
return {
|
||||
query : "erpnext.controllers.queries.get_batch_no",
|
||||
filters: {
|
||||
'item_code': this.item.item_code,
|
||||
'warehouse': this.item.s_warehouse || this.item.t_warehouse,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -297,11 +297,35 @@ class TestCustomer(FrappeTestCase):
|
||||
if credit_limit > outstanding_amt:
|
||||
set_credit_limit("_Test Customer", "_Test Company", credit_limit)
|
||||
|
||||
# Makes Sales invoice from Sales Order
|
||||
so.save(ignore_permissions=True)
|
||||
si = make_sales_invoice(so.name)
|
||||
si.save(ignore_permissions=True)
|
||||
self.assertRaises(frappe.ValidationError, make_sales_order)
|
||||
def test_customer_credit_limit_after_submit(self):
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
outstanding_amt = self.get_customer_outstanding_amount()
|
||||
credit_limit = get_credit_limit("_Test Customer", "_Test Company")
|
||||
|
||||
if outstanding_amt <= 0.0:
|
||||
item_qty = int((abs(outstanding_amt) + 200) / 100)
|
||||
make_sales_order(qty=item_qty)
|
||||
|
||||
if credit_limit <= 0.0:
|
||||
set_credit_limit("_Test Customer", "_Test Company", outstanding_amt + 100)
|
||||
|
||||
so = make_sales_order(rate=100, qty=1)
|
||||
# Update qty in submitted Sales Order to trigger Credit Limit validation
|
||||
fields = ["name", "item_code", "delivery_date", "conversion_factor", "qty", "rate", "uom", "idx"]
|
||||
modified_item = frappe._dict()
|
||||
for x in fields:
|
||||
modified_item[x] = so.items[0].get(x)
|
||||
modified_item["docname"] = so.items[0].name
|
||||
modified_item["qty"] = 2
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
update_child_qty_rate,
|
||||
so.doctype,
|
||||
frappe.json.dumps([modified_item]),
|
||||
so.name,
|
||||
)
|
||||
|
||||
def test_customer_credit_limit_on_change(self):
|
||||
outstanding_amt = self.get_customer_outstanding_amount()
|
||||
|
||||
@@ -249,7 +249,8 @@ class SalesOrder(SellingController):
|
||||
frappe.msgprint(
|
||||
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
|
||||
frappe.bold(so[0][0]), frappe.bold(self.po_no)
|
||||
)
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
@@ -513,6 +514,9 @@ class SalesOrder(SellingController):
|
||||
def on_update(self):
|
||||
pass
|
||||
|
||||
def on_update_after_submit(self):
|
||||
self.check_credit_limit()
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_po()
|
||||
self.validate_drop_ship()
|
||||
|
||||
@@ -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.sales_order.sales_order import (
|
||||
WarehouseRequired,
|
||||
create_pick_list,
|
||||
make_delivery_note,
|
||||
make_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.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):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
||||
@@ -206,42 +206,36 @@ def prepare_data(
|
||||
|
||||
def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_field):
|
||||
fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1)
|
||||
dates = [fiscal_year.year_start_date, fiscal_year.year_end_date]
|
||||
|
||||
select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field)
|
||||
child_table = "`tab{0}`".format(filters.get("doctype") + " Item")
|
||||
parent_doc = frappe.qb.DocType(filters.get("doctype"))
|
||||
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
|
||||
sales_team = frappe.qb.DocType("Sales Team")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent_doc)
|
||||
.inner_join(child_doc)
|
||||
.on(child_doc.parent == parent_doc.name)
|
||||
.inner_join(sales_team)
|
||||
.on(sales_team.parent == parent_doc.name)
|
||||
.select(
|
||||
child_doc.item_group,
|
||||
(child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
|
||||
(child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
|
||||
sales_team.sales_person,
|
||||
parent_doc[date_field],
|
||||
)
|
||||
.where(
|
||||
(parent_doc.docstatus == 1)
|
||||
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
|
||||
)
|
||||
)
|
||||
|
||||
if sales_field == "sales_person":
|
||||
select_field = "`tabSales Team`.sales_person"
|
||||
child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item")
|
||||
cond = """`tabSales Team`.parent = `tab{0}`.name and
|
||||
`tabSales Team`.sales_person in ({1}) """.format(
|
||||
filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data))
|
||||
)
|
||||
query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
|
||||
else:
|
||||
cond = "`tab{0}`.{1} in ({2})".format(
|
||||
filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data))
|
||||
)
|
||||
query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
|
||||
|
||||
return frappe.db.sql(
|
||||
""" SELECT `tab{child_doc}`.item_group,
|
||||
`tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount,
|
||||
{select_field}, `tab{parent_doc}`.{date_field}
|
||||
FROM `tab{parent_doc}`, {child_table}
|
||||
WHERE
|
||||
`tab{child_doc}`.parent = `tab{parent_doc}`.name
|
||||
and `tab{parent_doc}`.docstatus = 1 and {cond}
|
||||
and `tab{parent_doc}`.{date_field} between %s and %s""".format(
|
||||
cond=cond,
|
||||
date_field=date_field,
|
||||
select_field=select_field,
|
||||
child_table=child_table,
|
||||
parent_doc=filters.get("doctype"),
|
||||
child_doc=filters.get("doctype") + " Item",
|
||||
),
|
||||
tuple(sales_users_or_territory_data + dates),
|
||||
as_dict=1,
|
||||
)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_parents_data(filters, partner_doctype):
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.sales_person_target_variance_based_on_item_group import (
|
||||
execute,
|
||||
)
|
||||
|
||||
|
||||
class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_achieved_target_and_variance(self):
|
||||
# Create a Target Distribution
|
||||
distribution = frappe.new_doc("Monthly Distribution")
|
||||
distribution.distribution_id = "Target Report Distribution"
|
||||
distribution.fiscal_year = self.fiscal_year
|
||||
distribution.get_months()
|
||||
distribution.insert()
|
||||
|
||||
# Create sales people with targets
|
||||
person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
|
||||
person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
|
||||
|
||||
# Create a Sales Order with 50-50 contribution
|
||||
so = make_sales_order(
|
||||
rate=1000,
|
||||
qty=20,
|
||||
do_not_submit=True,
|
||||
)
|
||||
so.set(
|
||||
"sales_team",
|
||||
[
|
||||
{
|
||||
"sales_person": person_1.name,
|
||||
"allocated_percentage": 50,
|
||||
"allocated_amount": 10000,
|
||||
},
|
||||
{
|
||||
"sales_person": person_2.name,
|
||||
"allocated_percentage": 50,
|
||||
"allocated_amount": 10000,
|
||||
},
|
||||
],
|
||||
)
|
||||
so.submit()
|
||||
|
||||
# Check Achieved Target and Variance
|
||||
result = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"doctype": "Sales Order",
|
||||
"period": "Yearly",
|
||||
"target_on": "Quantity",
|
||||
}
|
||||
)
|
||||
)[1]
|
||||
row = frappe._dict(result[0])
|
||||
self.assertSequenceEqual(
|
||||
[flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
|
||||
[50, 10, -40],
|
||||
)
|
||||
|
||||
|
||||
def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
|
||||
sales_person = frappe.new_doc("Sales Person")
|
||||
sales_person.sales_person_name = sales_person_name
|
||||
sales_person.append(
|
||||
"targets",
|
||||
{
|
||||
"fiscal_year": fiscal_year,
|
||||
"target_qty": 50,
|
||||
"target_amount": 30000,
|
||||
"distribution_id": distribution_id,
|
||||
},
|
||||
)
|
||||
return sales_person.insert()
|
||||
@@ -36,6 +36,7 @@ def execute(filters=None):
|
||||
d.base_net_amount,
|
||||
d.sales_person,
|
||||
d.allocated_percentage,
|
||||
(d.stock_qty * d.allocated_percentage / 100),
|
||||
d.contribution_amt,
|
||||
company_currency,
|
||||
]
|
||||
@@ -103,7 +104,7 @@ def get_columns(filters):
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140},
|
||||
{"label": _("SO Total Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"options": "currency",
|
||||
@@ -119,6 +120,12 @@ def get_columns(filters):
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140},
|
||||
{
|
||||
"label": _("Contribution Qty"),
|
||||
"fieldname": "contribution_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Contribution Amount"),
|
||||
"options": "currency",
|
||||
|
||||
@@ -95,7 +95,17 @@ def create_demo_record(doctype):
|
||||
|
||||
def make_transactions(company):
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
|
||||
start_date = get_fiscal_year(date=getdate())[1]
|
||||
from erpnext.accounts.utils import FiscalYearError
|
||||
|
||||
try:
|
||||
start_date = get_fiscal_year(date=getdate())[1]
|
||||
except FiscalYearError:
|
||||
# User might have setup fiscal year for previous or upcoming years
|
||||
active_fiscal_years = frappe.db.get_all("Fiscal Year", filters={"disabled": 0}, as_list=1)
|
||||
if active_fiscal_years:
|
||||
start_date = frappe.db.get_value("Fiscal Year", active_fiscal_years[0][0], "year_start_date")
|
||||
else:
|
||||
frappe.throw(_("There are no active Fiscal Years for which Demo Data can be generated."))
|
||||
|
||||
for doctype in frappe.get_hooks("demo_transaction_doctypes"):
|
||||
data = read_data_file_using_hooks(doctype)
|
||||
@@ -159,6 +169,7 @@ def convert_order_to_invoices():
|
||||
|
||||
if i % 2 != 0:
|
||||
payment = get_payment_entry(invoice.doctype, invoice.name)
|
||||
payment.posting_date = order.transaction_date
|
||||
payment.reference_no = invoice.name
|
||||
payment.submit()
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ default_mail_footer = """<div style="padding: 7px; text-align: right; color: #88
|
||||
|
||||
|
||||
def after_install():
|
||||
frappe.get_doc({"doctype": "Role", "role_name": "Analytics"}).insert()
|
||||
if not frappe.db.exists("Role", "Analytics"):
|
||||
frappe.get_doc({"doctype": "Role", "role_name": "Analytics"}).insert()
|
||||
|
||||
set_single_defaults()
|
||||
create_print_setting_custom_fields()
|
||||
add_all_roles_to("Administrator")
|
||||
|
||||
@@ -8,9 +8,12 @@ from pypika import Order
|
||||
class DeprecatedSerialNoValuation:
|
||||
@deprecated
|
||||
def calculate_stock_value_from_deprecarated_ledgers(self):
|
||||
serial_nos = list(
|
||||
filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos())
|
||||
)
|
||||
if not frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
|
||||
):
|
||||
return
|
||||
|
||||
serial_nos = self.get_serial_nos()
|
||||
|
||||
actual_qty = flt(self.sle.actual_qty)
|
||||
|
||||
@@ -25,23 +28,12 @@ class DeprecatedSerialNoValuation:
|
||||
@deprecated
|
||||
def get_incoming_value_for_serial_nos(self, serial_nos):
|
||||
# get rate from serial nos within same company
|
||||
all_serial_nos = frappe.get_all(
|
||||
"Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
|
||||
)
|
||||
|
||||
incoming_values = 0.0
|
||||
for d in all_serial_nos:
|
||||
if d.company == self.sle.company:
|
||||
self.serial_no_incoming_rate[d.name] += flt(d.purchase_rate)
|
||||
incoming_values += flt(d.purchase_rate)
|
||||
|
||||
# Get rate for serial nos which has been transferred to other company
|
||||
invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company]
|
||||
for serial_no in invalid_serial_nos:
|
||||
for serial_no in serial_nos:
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
incoming_rate = (
|
||||
stock_ledgers = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.incoming_rate)
|
||||
.select(table.incoming_rate, table.actual_qty, table.stock_value_difference)
|
||||
.where(
|
||||
(
|
||||
(table.serial_no == serial_no)
|
||||
@@ -51,15 +43,18 @@ class DeprecatedSerialNoValuation:
|
||||
)
|
||||
& (table.company == self.sle.company)
|
||||
& (table.serial_and_batch_bundle.isnull())
|
||||
& (table.actual_qty > 0)
|
||||
& (table.is_cancelled == 0)
|
||||
)
|
||||
.orderby(table.posting_date, order=Order.desc)
|
||||
.limit(1)
|
||||
).run()
|
||||
.orderby(table.posting_datetime, order=Order.desc)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0
|
||||
incoming_values += self.serial_no_incoming_rate[serial_no]
|
||||
for sle in stock_ledgers:
|
||||
self.serial_no_incoming_rate[serial_no] += (
|
||||
flt(sle.incoming_rate)
|
||||
if sle.actual_qty > 0
|
||||
else (sle.stock_value_difference / sle.actual_qty) * -1
|
||||
)
|
||||
incoming_values += self.serial_no_incoming_rate[serial_no]
|
||||
|
||||
return incoming_values
|
||||
|
||||
|
||||
@@ -399,7 +399,12 @@ class DeliveryNote(SellingController):
|
||||
elif self.issue_credit_note:
|
||||
self.make_return_invoice()
|
||||
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
for table_name in ["items", "packed_items"]:
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -1067,6 +1067,8 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(si2.items[1].qty, 1)
|
||||
|
||||
def test_delivery_note_bundle_with_batched_item(self):
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
|
||||
batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0})
|
||||
batched_item = make_item(
|
||||
"_Test Batched Item",
|
||||
@@ -1088,6 +1090,8 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
@@ -1540,6 +1544,53 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(so.items[0].rate, rate)
|
||||
self.assertEqual(dn.items[0].rate, so.items[0].rate)
|
||||
|
||||
def test_use_serial_batch_fields_for_packed_items(self):
|
||||
bundle_item = make_item("Test _Packed Product Bundle Item ", {"is_stock_item": 0})
|
||||
serial_item = make_item(
|
||||
"Test _Packed Serial Item ",
|
||||
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-TESTSERIAL-.#####"},
|
||||
)
|
||||
batch_item = make_item(
|
||||
"Test _Packed Batch Item ",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_no_series": "BATCH-TESTSERIAL-.#####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
)
|
||||
make_product_bundle(parent=bundle_item.name, items=[serial_item.name, batch_item.name])
|
||||
|
||||
item_details = {}
|
||||
for item in [serial_item, batch_item]:
|
||||
se = make_stock_entry(
|
||||
item_code=item.name, target="_Test Warehouse - _TC", qty=5, basic_rate=100
|
||||
)
|
||||
item_details[item.name] = se.items[0].serial_and_batch_bundle
|
||||
|
||||
dn = create_delivery_note(item_code=bundle_item.name, qty=1, do_not_submit=True)
|
||||
serial_no = ""
|
||||
for row in dn.packed_items:
|
||||
row.use_serial_batch_fields = 1
|
||||
|
||||
if row.item_code == serial_item.name:
|
||||
serial_and_batch_bundle = item_details[serial_item.name]
|
||||
row.serial_no = get_serial_nos_from_bundle(serial_and_batch_bundle)[3]
|
||||
serial_no = row.serial_no
|
||||
else:
|
||||
serial_and_batch_bundle = item_details[batch_item.name]
|
||||
row.batch_no = get_batch_from_bundle(serial_and_batch_bundle)
|
||||
|
||||
dn.submit()
|
||||
dn.load_from_db()
|
||||
|
||||
for row in dn.packed_items:
|
||||
self.assertTrue(row.serial_no or row.batch_no)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
if row.serial_no:
|
||||
self.assertEqual(row.serial_no, serial_no)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -1122,6 +1122,7 @@ def validate_cancelled_item(item_code, docstatus=None):
|
||||
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):
|
||||
"""returns last purchase details in stock uom"""
|
||||
# get last purchase order item details
|
||||
|
||||
@@ -1,187 +1,77 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-02-22 01:28:02",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2013-02-22 01:28:02",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"receipt_document_type",
|
||||
"receipt_document",
|
||||
"supplier",
|
||||
"col_break1",
|
||||
"posting_date",
|
||||
"grand_total"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "receipt_document_type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Receipt Document Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "\nPurchase Invoice\nPurchase Receipt",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "receipt_document_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Receipt Document Type",
|
||||
"options": "\nPurchase Invoice\nPurchase Receipt",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "receipt_document",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Receipt Document",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "purchase_receipt_no",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "receipt_document_type",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "220px",
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "receipt_document",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Receipt Document",
|
||||
"oldfieldname": "purchase_receipt_no",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "receipt_document_type",
|
||||
"print_width": "220px",
|
||||
"reqd": 1,
|
||||
"width": "220px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Supplier",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Supplier",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplier",
|
||||
"options": "Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Posting Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Grand Total",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Grand Total",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Landed Cost Purchase Receipt",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_order": "ASC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-26 18:41:06.281750",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Landed Cost Purchase Receipt",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -65,6 +65,7 @@ class LandedCostVoucher(Document):
|
||||
def validate(self):
|
||||
self.check_mandatory()
|
||||
self.validate_receipt_documents()
|
||||
self.validate_line_items()
|
||||
init_landed_taxes_and_totals(self)
|
||||
self.set_total_taxes_and_charges()
|
||||
if not self.get("items"):
|
||||
@@ -72,6 +73,26 @@ class LandedCostVoucher(Document):
|
||||
|
||||
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):
|
||||
if not self.get("purchase_receipts"):
|
||||
frappe.throw(_("Please enter Receipt Document"))
|
||||
|
||||
@@ -199,6 +199,7 @@ frappe.ui.form.on('Material Request', {
|
||||
|
||||
get_item_data: function(frm, item, overwrite_warehouse=false) {
|
||||
if (item && !item.item_code) { return; }
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_details",
|
||||
args: {
|
||||
@@ -225,12 +226,22 @@ frappe.ui.form.on('Material Request', {
|
||||
},
|
||||
callback: function(r) {
|
||||
const d = item;
|
||||
const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty'];
|
||||
const allow_to_change_fields = ['actual_qty', 'projected_qty', 'min_order_qty', 'item_name', 'description', 'stock_uom', 'uom', 'conversion_factor', 'stock_qty'];
|
||||
|
||||
if(!r.exc) {
|
||||
$.each(r.message, function(k, v) {
|
||||
if(!d[k] || in_list(qty_fields, k)) d[k] = v;
|
||||
$.each(r.message, function(key, value) {
|
||||
if(!d[key] || allow_to_change_fields.includes(key)) {
|
||||
d[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (d.price_list_rate != r.message.price_list_rate) {
|
||||
d.rate = 0.0;
|
||||
d.price_list_rate = r.message.price_list_rate;
|
||||
frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate);
|
||||
}
|
||||
|
||||
refresh_field("items");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -242,7 +253,7 @@ frappe.ui.form.on('Material Request', {
|
||||
fields: [
|
||||
{"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"),
|
||||
options:"BOM", reqd: 1, get_query: function() {
|
||||
return {filters: { docstatus:1 }};
|
||||
return {filters: { docstatus:1, "is_active": 1 }};
|
||||
}},
|
||||
{"fieldname":"warehouse", "fieldtype":"Link", "label":__("For Warehouse"),
|
||||
options:"Warehouse", reqd: 1},
|
||||
@@ -427,12 +438,11 @@ frappe.ui.form.on("Material Request Item", {
|
||||
frm.events.get_item_data(frm, item, false);
|
||||
},
|
||||
|
||||
rate: function(frm, doctype, name) {
|
||||
rate(frm, doctype, name) {
|
||||
const item = locals[doctype][name];
|
||||
item.amount = flt(item.qty) * flt(item.rate);
|
||||
frappe.model.set_value(doctype, name, "amount", item.amount);
|
||||
refresh_field("amount", item.name, item.parentfield);
|
||||
frm.events.get_item_data(frm, item, false);
|
||||
},
|
||||
|
||||
item_code: function(frm, doctype, name) {
|
||||
@@ -452,7 +462,12 @@ frappe.ui.form.on("Material Request Item", {
|
||||
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 {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"received_qty",
|
||||
"rate_and_amount_section_break",
|
||||
"rate",
|
||||
"price_list_rate",
|
||||
"col_break3",
|
||||
"amount",
|
||||
"accounting_details_section",
|
||||
@@ -197,12 +198,14 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -473,13 +476,22 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite 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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:37:59.599115",
|
||||
"modified": "2024-02-26 18:30:03.684872",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request Item",
|
||||
|
||||
@@ -41,6 +41,7 @@ class MaterialRequestItem(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
price_list_rate: DF.Currency
|
||||
production_plan: DF.Link | None
|
||||
project: DF.Link | None
|
||||
projected_qty: DF.Float
|
||||
|
||||
@@ -227,6 +227,9 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
|
||||
bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse)
|
||||
pi_row.actual_qty = flt(bin.get("actual_qty"))
|
||||
pi_row.projected_qty = flt(bin.get("projected_qty"))
|
||||
pi_row.use_serial_batch_fields = frappe.db.get_single_value(
|
||||
"Stock Settings", "use_serial_batch_fields"
|
||||
)
|
||||
|
||||
|
||||
def update_packed_item_price_data(pi_row, item_data, doc):
|
||||
|
||||
@@ -77,6 +77,9 @@ frappe.ui.form.on('Pick List', {
|
||||
},
|
||||
freeze: 1,
|
||||
freeze_message: __("Setting Item Locations..."),
|
||||
callback(r) {
|
||||
refresh_field("locations");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -327,7 +330,8 @@ frappe.ui.form.on('Pick List Item', {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": qty
|
||||
"use_serial_batch_fields": 0,
|
||||
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user