mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-01 16:54:36 +00:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eefac19cc8 | ||
|
|
afc049154d | ||
|
|
8657ba374a | ||
|
|
3a8b7e9b96 | ||
|
|
22b442d593 | ||
|
|
f7bfbd82dd | ||
|
|
d375164100 | ||
|
|
7fdac62393 | ||
|
|
2184e8ef58 | ||
|
|
857843bc29 | ||
|
|
e0c8ff10da | ||
|
|
61b02ec75c | ||
|
|
939b7f7cbf | ||
|
|
77da0da945 | ||
|
|
4114760efd | ||
|
|
23042dfc3c | ||
|
|
9216a852d4 | ||
|
|
6478c80359 | ||
|
|
e18dc5cea3 | ||
|
|
fb5090fd3f | ||
|
|
20a764a4a3 | ||
|
|
24ae45ce84 | ||
|
|
a14e7f0e84 | ||
|
|
942f34a734 | ||
|
|
4821919695 | ||
|
|
34549b62bd | ||
|
|
d593f81a16 | ||
|
|
e05b23c388 | ||
|
|
d4c131b153 | ||
|
|
9836205725 | ||
|
|
12cd7a7083 | ||
|
|
e539152c6b | ||
|
|
6ed7c6610a | ||
|
|
d6d717c315 | ||
|
|
935e23449f | ||
|
|
ed9b38bc8d | ||
|
|
2c52f2ce13 | ||
|
|
1469ca7d0b | ||
|
|
73d525e84d | ||
|
|
f6c611d026 | ||
|
|
30b6321fd5 | ||
|
|
20a26c3dd4 | ||
|
|
d975eccaab | ||
|
|
cdef0bd503 | ||
|
|
a714114cd9 | ||
|
|
bf585de558 | ||
|
|
b7fd24074d | ||
|
|
d53a9f6530 | ||
|
|
dda95ea892 | ||
|
|
4908d03a88 | ||
|
|
229075048b | ||
|
|
0d49dd3648 | ||
|
|
214861d68f | ||
|
|
e7b96209cd | ||
|
|
753a1b511d | ||
|
|
6715dd6167 | ||
|
|
a061798a6f | ||
|
|
fa79a7cdab | ||
|
|
6884aac2a0 | ||
|
|
e61a12b4cb | ||
|
|
4542a12787 | ||
|
|
f0408c89ce | ||
|
|
7c080bd60c | ||
|
|
c87a3c7793 | ||
|
|
038a30feb0 | ||
|
|
c63f873a6f | ||
|
|
df93447d30 | ||
|
|
7477ff3b5c | ||
|
|
812b301aab | ||
|
|
9dfe342838 | ||
|
|
7813efad35 | ||
|
|
29cd474f44 | ||
|
|
008400d287 | ||
|
|
4b9d9c0df4 | ||
|
|
b28deaae2c | ||
|
|
4e6b8ead8f | ||
|
|
c2a137f0fb | ||
|
|
f6fcb7ee26 | ||
|
|
cbe15e159a |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.53.0"
|
||||
__version__ = "14.55.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -423,9 +423,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.add_payment_entries(vouchers)
|
||||
transaction.save()
|
||||
|
||||
return transaction
|
||||
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"status",
|
||||
"bank_account",
|
||||
"company",
|
||||
"amended_from",
|
||||
"section_break_4",
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
@@ -26,10 +25,10 @@
|
||||
"transaction_id",
|
||||
"transaction_type",
|
||||
"section_break_14",
|
||||
"column_break_oufv",
|
||||
"payment_entries",
|
||||
"section_break_18",
|
||||
"allocated_amount",
|
||||
"amended_from",
|
||||
"column_break_17",
|
||||
"unallocated_amount",
|
||||
"party_section",
|
||||
@@ -139,12 +138,10 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@@ -160,12 +157,10 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "unallocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Unallocated Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_section",
|
||||
@@ -230,15 +225,11 @@
|
||||
"fieldname": "bank_party_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Account No. (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified": "2023-06-06 13:58:12.821411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -2,73 +2,78 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
|
||||
class BankTransaction(StatusUpdater):
|
||||
def before_validate(self):
|
||||
self.update_allocated_amount()
|
||||
def after_insert(self):
|
||||
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
|
||||
def validate(self):
|
||||
self.validate_duplicate_references()
|
||||
|
||||
def validate_duplicate_references(self):
|
||||
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
|
||||
if not self.payment_entries:
|
||||
return
|
||||
|
||||
pe = []
|
||||
for row in self.payment_entries:
|
||||
reference = (row.payment_document, row.payment_entry)
|
||||
if reference in pe:
|
||||
frappe.throw(
|
||||
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
||||
row.payment_document, row.payment_entry
|
||||
)
|
||||
)
|
||||
pe.append(reference)
|
||||
|
||||
def update_allocated_amount(self):
|
||||
self.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
|
||||
|
||||
def before_submit(self):
|
||||
self.allocate_payment_entries()
|
||||
def on_submit(self):
|
||||
self.clear_linked_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_duplicate_references()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
_saving_flag = False
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
def on_update_after_submit(self):
|
||||
"Run on save(). Avoid recursion caused by multiple saves"
|
||||
if not self._saving_flag:
|
||||
self._saving_flag = True
|
||||
self.clear_linked_payment_entries()
|
||||
self.update_allocations()
|
||||
self._saving_flag = False
|
||||
|
||||
def on_cancel(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.clear_linked_payment_entries(for_cancel=True)
|
||||
self.set_status(update=True)
|
||||
|
||||
def update_allocations(self):
|
||||
"The doctype does not allow modifications after submission, so write to the db direct"
|
||||
if self.payment_entries:
|
||||
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
|
||||
else:
|
||||
allocated_amount = 0.0
|
||||
|
||||
amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
self.db_set("allocated_amount", flt(allocated_amount))
|
||||
self.db_set("unallocated_amount", amount - flt(allocated_amount))
|
||||
self.reload()
|
||||
self.set_status(update=True)
|
||||
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
if 0.0 >= self.unallocated_amount:
|
||||
frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
|
||||
added = False
|
||||
for voucher in vouchers:
|
||||
self.append(
|
||||
"payment_entries",
|
||||
{
|
||||
# Can't add same voucher twice
|
||||
found = False
|
||||
for pe in self.payment_entries:
|
||||
if (
|
||||
pe.payment_document == voucher["payment_doctype"]
|
||||
and pe.payment_entry == voucher["payment_name"]
|
||||
):
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
pe = {
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": 0.0, # Temporary
|
||||
},
|
||||
)
|
||||
}
|
||||
child = self.append("payment_entries", pe)
|
||||
added = True
|
||||
|
||||
# runs on_update_after_submit
|
||||
if added:
|
||||
self.save()
|
||||
|
||||
def allocate_payment_entries(self):
|
||||
"""Refactored from bank reconciliation tool.
|
||||
@@ -85,7 +90,6 @@ class BankTransaction(StatusUpdater):
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
@@ -95,39 +99,49 @@ class BankTransaction(StatusUpdater):
|
||||
if 0.0 == unallocated_amount:
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
|
||||
elif remaining_amount <= 0.0:
|
||||
to_remove.append(payment_entry)
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount <= remaining_amount:
|
||||
payment_entry.allocated_amount = unallocated_amount
|
||||
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", unallocated_amount)
|
||||
remaining_amount -= unallocated_amount
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount:
|
||||
payment_entry.allocated_amount = remaining_amount
|
||||
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", remaining_amount)
|
||||
remaining_amount = 0.0
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
for payment_entry in to_remove:
|
||||
self.remove(to_remove)
|
||||
self.reload()
|
||||
|
||||
def db_delete_payment_entry(self, payment_entry):
|
||||
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.remove_payment_entry(payment_entry)
|
||||
|
||||
self.save() # runs before_update_after_submit
|
||||
# runs on_update_after_submit
|
||||
self.save()
|
||||
|
||||
def remove_payment_entry(self, payment_entry):
|
||||
"Clear payment entry and clearance"
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.remove(payment_entry)
|
||||
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
if for_cancel:
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel)
|
||||
else:
|
||||
self.allocate_payment_entries()
|
||||
|
||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||
clearance_date = None if for_cancel else self.date
|
||||
set_voucher_clearance(
|
||||
@@ -148,10 +162,11 @@ class BankTransaction(StatusUpdater):
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
self.party_type, self.party = result
|
||||
if result:
|
||||
party_type, party = result
|
||||
frappe.db.set_value(
|
||||
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -183,7 +198,9 @@ def get_clearance_details(transaction, payment_entry):
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
|
||||
frappe._("Voucher {0} value is broken: {1}").format(
|
||||
payment_entry.payment_entry, gle["amount"]
|
||||
)
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
@@ -204,7 +221,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
|
||||
def get_related_bank_gl_entries(doctype, docname):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
return frappe.db.sql(
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||
@@ -222,6 +239,7 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
@@ -354,7 +372,6 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if clearance_date:
|
||||
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||
bt.add_payment_entries(vouchers)
|
||||
bt.save()
|
||||
else:
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||
|
||||
@@ -111,8 +111,6 @@ class PaymentReconciliation(Document):
|
||||
def get_payment_entries(self):
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = self.get_conditions(get_payments=True)
|
||||
if self.payment_name:
|
||||
condition += "name like '%%{0}%%'".format(self.payment_name)
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
self.party_type,
|
||||
@@ -122,6 +120,7 @@ class PaymentReconciliation(Document):
|
||||
against_all_orders=True,
|
||||
limit=self.payment_limit,
|
||||
condition=condition,
|
||||
payment_name=self.payment_name,
|
||||
)
|
||||
|
||||
return payment_entries
|
||||
@@ -539,6 +538,27 @@ class PaymentReconciliation(Document):
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
journals = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||
]
|
||||
journals.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||
)
|
||||
if journals:
|
||||
journals = list(set(journals))
|
||||
journals_map = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
|
||||
fields=[
|
||||
"parent as `name`",
|
||||
"exchange_rate",
|
||||
],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
invoice_exchange_map.update(journals_map)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
|
||||
@@ -6,4 +6,6 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentReconciliationAllocation(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -6,4 +6,6 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentReconciliationInvoice(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -6,4 +6,6 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentReconciliationPayment(Document):
|
||||
pass
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"group_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
"column_break_14",
|
||||
"to_date",
|
||||
"finance_book",
|
||||
@@ -374,10 +375,16 @@
|
||||
"fieldname": "pdf_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "PDF Name"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2023-08-28 12:59:53.071334",
|
||||
"modified": "2023-12-18 12:20:08.965120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -64,6 +64,18 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
statement_dict = {}
|
||||
ageing = ""
|
||||
|
||||
err_journals = None
|
||||
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
|
||||
err_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": doc.company,
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
for entry in doc.customers:
|
||||
if doc.include_ageing:
|
||||
ageing = set_ageing(doc, entry)
|
||||
@@ -76,6 +88,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
||||
)
|
||||
|
||||
filters = get_common_filters(doc)
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if doc.report == "General Ledger":
|
||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||
|
||||
@@ -108,7 +108,7 @@ class RepostAccountingLedger(Document):
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
if len(self.vouchers) > 1:
|
||||
if len(self.vouchers) > 5:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
@@ -152,8 +152,6 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
|
||||
@@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.update_repost_settings()
|
||||
update_repost_settings()
|
||||
|
||||
def teadDown(self):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def update_repost_settings(self):
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
@@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
# Submit repost document
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
res = (
|
||||
qb.from_(gl)
|
||||
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
|
||||
@@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save()
|
||||
|
||||
# assert preview data is generated
|
||||
preview = ral.generate_preview()
|
||||
self.assertIsNotNone(preview)
|
||||
|
||||
ral.save().submit()
|
||||
|
||||
# background jobs don't run on test cases. Manually triggering repost function.
|
||||
start_repost(ral.name)
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
@@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
start_repost(ral.name)
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_05_without_deletion_flag(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.save().submit()
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
ral.save().submit()
|
||||
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
|
||||
@@ -2195,9 +2195,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
|
||||
filters = {
|
||||
reference_field: reference_name,
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
|
||||
filters=filters,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -2791,6 +2791,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
|
||||
@@ -244,8 +244,12 @@ class ReceivablePayableReport(object):
|
||||
row.invoiced_in_account_currency += amount_in_account_currency
|
||||
else:
|
||||
if self.is_invoice(ple):
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
row.credit_note -= amount
|
||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||
else:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
return credit_note
|
||||
|
||||
def test_pos_receivable(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
"report_date": add_days(today(), 2),
|
||||
"based_on_payment_terms": 0,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": False,
|
||||
}
|
||||
|
||||
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
pos_inv.posting_date = add_days(today(), 2)
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
frappe._dict(
|
||||
mode_of_payment="Cash",
|
||||
amount=flt(pos_inv.grand_total / 2),
|
||||
),
|
||||
)
|
||||
pos_inv.disable_rounded_total = 1
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
report = execute(filters)
|
||||
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
|
||||
|
||||
row = report[1][-1]
|
||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||
pos_inv.cancel()
|
||||
|
||||
def test_accounts_receivable(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -8,7 +8,17 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
getdate,
|
||||
today,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@@ -43,6 +53,8 @@ def get_period_list(
|
||||
year_start_date = getdate(period_start_date)
|
||||
year_end_date = getdate(period_end_date)
|
||||
|
||||
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
|
||||
|
||||
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
|
||||
|
||||
period_list = []
|
||||
|
||||
@@ -231,6 +231,9 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no"):
|
||||
conditions.append("voucher_no=%(voucher_no)s")
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
|
||||
@@ -626,8 +626,10 @@ def update_reference_in_payment_entry(
|
||||
"total_amount": d.grand_total,
|
||||
"outstanding_amount": d.outstanding_amount,
|
||||
"allocated_amount": d.allocated_amount,
|
||||
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.exchange_gain_loss,
|
||||
"exchange_rate": d.exchange_rate
|
||||
if d.difference_amount is not None
|
||||
else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.difference_amount,
|
||||
}
|
||||
|
||||
if d.voucher_detail_no:
|
||||
|
||||
@@ -427,6 +427,10 @@ class Asset(AccountsController):
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
and get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
self, value_after_depreciation, finance_book, False
|
||||
)
|
||||
== finance_book.rate_of_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
self.available_for_use_date, -1
|
||||
@@ -1387,7 +1391,9 @@ def get_depreciation_amount(
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
|
||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
asset, depreciable_value, fb_row, show_msg=True
|
||||
):
|
||||
return fb_row.rate_of_depreciation
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +80,15 @@ class RequestforQuotation(BuyingController):
|
||||
supplier.quote_status = "Pending"
|
||||
self.send_to_supplier()
|
||||
|
||||
def before_print(self, settings=None):
|
||||
"""Use the first suppliers data to render the print preview."""
|
||||
if self.vendor or not self.suppliers:
|
||||
# If a specific supplier is already set, via Tools > Download PDF,
|
||||
# we don't want to override it.
|
||||
return
|
||||
|
||||
self.update_supplier_part_no(self.suppliers[0].supplier)
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ class AccountsController(TransactionBase):
|
||||
self.disable_pricing_rule_on_internal_transfer()
|
||||
self.disable_tax_included_prices_for_internal_transfer()
|
||||
self.set_incoming_rate()
|
||||
self.init_internal_values()
|
||||
|
||||
if self.meta.get_field("currency"):
|
||||
self.calculate_taxes_and_totals()
|
||||
@@ -224,6 +225,16 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_total_in_words()
|
||||
|
||||
def init_internal_values(self):
|
||||
# init all the internal values as 0 on sa
|
||||
if self.docstatus.is_draft():
|
||||
# TODO: Add all such pending values here
|
||||
fields = ["billed_amt", "delivered_qty"]
|
||||
for item in self.get("items"):
|
||||
for field in fields:
|
||||
if hasattr(item, field):
|
||||
item.set(field, 0)
|
||||
|
||||
def before_cancel(self):
|
||||
validate_einvoice_fields(self)
|
||||
|
||||
@@ -629,6 +640,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
args["doctype"] = self.doctype
|
||||
args["name"] = self.name
|
||||
args["child_doctype"] = item.doctype
|
||||
args["child_docname"] = item.name
|
||||
args["ignore_pricing_rule"] = (
|
||||
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
|
||||
@@ -2504,6 +2516,7 @@ def get_advance_payment_entries(
|
||||
against_all_orders=False,
|
||||
limit=None,
|
||||
condition=None,
|
||||
payment_name=None,
|
||||
):
|
||||
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
|
||||
currency_field = (
|
||||
@@ -2526,6 +2539,10 @@ def get_advance_payment_entries(
|
||||
reference_condition = ""
|
||||
order_list = []
|
||||
|
||||
payment_name_filter = ""
|
||||
if payment_name:
|
||||
payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name)
|
||||
|
||||
if not condition:
|
||||
condition = ""
|
||||
|
||||
@@ -2540,7 +2557,7 @@ def get_advance_payment_entries(
|
||||
where
|
||||
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
|
||||
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
|
||||
and t2.reference_doctype = %s {2} {3}
|
||||
and t2.reference_doctype = %s {2} {3} {6}
|
||||
order by t1.posting_date {4}
|
||||
""".format(
|
||||
currency_field,
|
||||
@@ -2549,12 +2566,17 @@ def get_advance_payment_entries(
|
||||
condition,
|
||||
limit_cond,
|
||||
exchange_rate_field,
|
||||
payment_name_filter,
|
||||
),
|
||||
[party_account, payment_type, party_type, party, order_doctype] + order_list,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if include_unallocated:
|
||||
payment_name_filter = ""
|
||||
if payment_name:
|
||||
payment_name_filter = " and name like '%%{0}%%'".format(payment_name)
|
||||
|
||||
unallocated_payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select 'Payment Entry' as reference_type, name as reference_name, posting_date,
|
||||
@@ -2562,10 +2584,15 @@ def get_advance_payment_entries(
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
{0} = %s and party_type = %s and party = %s and payment_type = %s
|
||||
and docstatus = 1 and unallocated_amount > 0 {condition}
|
||||
and docstatus = 1 and unallocated_amount > 0 {condition} {4}
|
||||
order by posting_date {1}
|
||||
""".format(
|
||||
party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""
|
||||
party_account_field,
|
||||
limit_cond,
|
||||
exchange_rate_field,
|
||||
currency_field,
|
||||
payment_name_filter,
|
||||
condition=condition or "",
|
||||
),
|
||||
(party_account, party_type, party, payment_type),
|
||||
as_dict=1,
|
||||
|
||||
@@ -354,7 +354,11 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
field = (
|
||||
"incoming_rate"
|
||||
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
|
||||
else "rate"
|
||||
)
|
||||
rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
|
||||
@@ -52,10 +52,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part
|
||||
|
||||
copy_attributes_to_variant(template, variant)
|
||||
|
||||
variant.manufacturer = manufacturer
|
||||
variant.manufacturer_part_no = manufacturer_part_no
|
||||
|
||||
variant.item_code = append_number_if_name_exists("Item", template.name)
|
||||
variant.flags.ignore_mandatory = True
|
||||
variant.save()
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}
|
||||
):
|
||||
manufacturer_doc = frappe.new_doc("Item Manufacturer")
|
||||
manufacturer_doc.update(
|
||||
{
|
||||
"item_code": variant.name,
|
||||
"manufacturer": manufacturer,
|
||||
"manufacturer_part_no": manufacturer_part_no,
|
||||
}
|
||||
)
|
||||
|
||||
manufacturer_doc.flags.ignore_mandatory = True
|
||||
manufacturer_doc.save(ignore_permissions=True)
|
||||
|
||||
return variant
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.item.item import set_item_default
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
@@ -422,11 +422,13 @@ class SellingController(StockController):
|
||||
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
if not self.get("return_against"):
|
||||
if not self.get("return_against") or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
||||
|
||||
if not (self.get("is_return") and d.incoming_rate):
|
||||
if not d.incoming_rate:
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
|
||||
@@ -514,7 +514,7 @@
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2022-10-13 12:42:04.277879",
|
||||
"modified": "2023-12-01 18:46:49.468526",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -576,10 +576,11 @@
|
||||
],
|
||||
"search_fields": "lead_name,lead_owner,status",
|
||||
"sender_field": "email_id",
|
||||
"sender_name_field": "lead_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"subject_field": "title",
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
from erpnext.selling.doctype.customer.customer import parse_full_name
|
||||
|
||||
|
||||
class Lead(SellingController, CRMNote):
|
||||
@@ -48,6 +49,10 @@ class Lead(SellingController, CRMNote):
|
||||
return
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
# leads created by email inbox only have the full name set
|
||||
if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]):
|
||||
self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name)
|
||||
|
||||
def after_insert(self):
|
||||
self.link_to_contact()
|
||||
|
||||
|
||||
@@ -638,4 +638,5 @@ additional_timeline_content = {
|
||||
|
||||
extend_bootinfo = [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
|
||||
"erpnext.startup.boot.bootinfo",
|
||||
]
|
||||
|
||||
@@ -599,6 +599,8 @@ def regenerate_repayment_schedule(loan, cancel=0):
|
||||
last_repayment_amount = None
|
||||
last_balance_amount = None
|
||||
|
||||
original_repayment_schedule_len = len(loan_doc.get("repayment_schedule"))
|
||||
|
||||
for term in reversed(loan_doc.get("repayment_schedule")):
|
||||
if not term.is_accrued:
|
||||
next_accrual_date = term.payment_date
|
||||
@@ -616,7 +618,7 @@ def regenerate_repayment_schedule(loan, cancel=0):
|
||||
|
||||
if loan_doc.repayment_method == "Repay Fixed Amount per Period":
|
||||
monthly_repayment_amount = flt(
|
||||
balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
|
||||
balance_amount / (original_repayment_schedule_len - accrued_entries)
|
||||
)
|
||||
else:
|
||||
repayment_period = loan_doc.repayment_periods - accrued_entries
|
||||
|
||||
@@ -661,6 +661,9 @@ class BOM(WebsiteGenerator):
|
||||
base_total_rm_cost = 0
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate":
|
||||
continue
|
||||
|
||||
old_rate = d.rate
|
||||
d.rate = self.get_rm_rate(
|
||||
{
|
||||
|
||||
@@ -698,6 +698,35 @@ class TestBOM(FrappeTestCase):
|
||||
bom.update_cost()
|
||||
self.assertFalse(bom.flags.cost_updated)
|
||||
|
||||
def test_bom_with_service_item_cost(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 1000.0}).name
|
||||
|
||||
service_item = make_item(properties={"is_stock_item": 0}).name
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item, service_item], do_not_save=True)
|
||||
bom.rm_cost_as_per = "Valuation Rate"
|
||||
|
||||
for row in bom.items:
|
||||
if row.item_code == service_item:
|
||||
row.rate = 566.00
|
||||
else:
|
||||
row.rate = 800.00
|
||||
|
||||
bom.save()
|
||||
|
||||
for row in bom.items:
|
||||
if row.item_code == service_item:
|
||||
self.assertEqual(row.is_stock_item, 0)
|
||||
self.assertEqual(row.rate, 566.00)
|
||||
else:
|
||||
self.assertEqual(row.is_stock_item, 1)
|
||||
|
||||
def test_do_not_include_manufacturing_and_fixed_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import item_query
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"bom_no",
|
||||
"source_warehouse",
|
||||
"allow_alternative_item",
|
||||
"is_stock_item",
|
||||
"section_break_5",
|
||||
"description",
|
||||
"col_break1",
|
||||
@@ -185,7 +186,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:doc.is_stock_item == 1",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -284,13 +285,21 @@
|
||||
"fieldname": "do_not_explode",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do Not Explode"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.is_stock_item",
|
||||
"fieldname": "is_stock_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Stock Item",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:35:51.378513",
|
||||
"modified": "2023-12-20 16:21:55.477883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Item",
|
||||
|
||||
@@ -508,6 +508,7 @@ class ProductionPlan(Document):
|
||||
|
||||
if close:
|
||||
self.db_set("status", "Closed")
|
||||
self.update_bin_qty()
|
||||
return
|
||||
|
||||
if self.total_produced_qty > 0:
|
||||
@@ -522,6 +523,9 @@ class ProductionPlan(Document):
|
||||
if close is not None:
|
||||
self.db_set("status", self.status)
|
||||
|
||||
if self.docstatus == 1 and self.status != "Completed":
|
||||
self.update_bin_qty()
|
||||
|
||||
def update_ordered_status(self):
|
||||
update_status = False
|
||||
for d in self.po_items:
|
||||
|
||||
@@ -1447,6 +1447,47 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertEqual(row.get("uom"), "Nos")
|
||||
self.assertEqual(row.get("conversion_factor"), 10.0)
|
||||
|
||||
def test_unreserve_qty_on_closing_of_pp(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
|
||||
store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
|
||||
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1
|
||||
)
|
||||
|
||||
pln.for_warehouse = rm_warehouse
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
pln.append("mr_items", d)
|
||||
|
||||
pln.save()
|
||||
pln.submit()
|
||||
|
||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
pln.reload()
|
||||
pln.set_status(close=True)
|
||||
|
||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty - 10)
|
||||
|
||||
pln.reload()
|
||||
pln.set_status(close=False)
|
||||
|
||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
self.assertAlmostEqual(after_qty, before_qty)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -354,3 +354,5 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
|
||||
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||
# 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
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
19
erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py
Normal file
19
erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("BOM", {"docstatus": 1}):
|
||||
return
|
||||
|
||||
# Added is_stock_item to handle Read Only based on condition for the rate field
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE
|
||||
`tabBOM Item` boi,
|
||||
`tabItem` i
|
||||
SET
|
||||
boi.is_stock_item = i.is_stock_item
|
||||
WHERE
|
||||
boi.item_code = i.name
|
||||
"""
|
||||
)
|
||||
@@ -482,6 +482,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
cost_center: item.cost_center,
|
||||
tax_category: me.frm.doc.tax_category,
|
||||
item_tax_template: item.item_tax_template,
|
||||
child_doctype: item.doctype,
|
||||
child_docname: item.name,
|
||||
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ frappe.provide("erpnext.financial_statements");
|
||||
|
||||
erpnext.financial_statements = {
|
||||
"filters": get_filters(),
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
"formatter": function(value, row, column, data, default_formatter, filter) {
|
||||
if (data && column.fieldname=="account") {
|
||||
value = data.account_name || value;
|
||||
|
||||
if (filter && filter?.text && filter?.type == "contains") {
|
||||
if (!value.toLowerCase().includes(filter.text)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
|
||||
@@ -971,7 +971,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
||||
}
|
||||
|
||||
function get_time_left(timestamp, agreement_status) {
|
||||
const diff = moment(timestamp).diff(moment());
|
||||
const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true));
|
||||
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
|
||||
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
|
||||
return {'diff_display': diff_display, 'indicator': indicator};
|
||||
|
||||
@@ -307,18 +307,19 @@ class Customer(TransactionBase):
|
||||
|
||||
def create_contact(contact, party_type, party, email):
|
||||
"""Create contact based on given contact name"""
|
||||
contact = contact.split(" ")
|
||||
|
||||
contact = frappe.get_doc(
|
||||
first, middle, last = parse_full_name(contact)
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": contact[0],
|
||||
"last_name": len(contact) > 1 and contact[1] or "",
|
||||
"first_name": first,
|
||||
"middle_name": middle,
|
||||
"last_name": last,
|
||||
"is_primary_contact": 1,
|
||||
}
|
||||
)
|
||||
contact.append("email_ids", dict(email_id=email, is_primary=1))
|
||||
contact.append("links", dict(link_doctype=party_type, link_name=party))
|
||||
contact.insert()
|
||||
doc.append("email_ids", dict(email_id=email, is_primary=1))
|
||||
doc.append("links", dict(link_doctype=party_type, link_name=party))
|
||||
return doc.insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -684,24 +685,42 @@ def get_credit_limit(customer, company):
|
||||
|
||||
|
||||
def make_contact(args, is_primary_contact=1):
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": args.get("name"),
|
||||
"is_primary_contact": is_primary_contact,
|
||||
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
|
||||
}
|
||||
)
|
||||
values = {
|
||||
"doctype": "Contact",
|
||||
"is_primary_contact": is_primary_contact,
|
||||
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
|
||||
}
|
||||
if args.customer_type == "Individual":
|
||||
first, middle, last = parse_full_name(args.get("customer_name"))
|
||||
values.update(
|
||||
{
|
||||
"first_name": first,
|
||||
"middle_name": middle,
|
||||
"last_name": last,
|
||||
}
|
||||
)
|
||||
else:
|
||||
values.update(
|
||||
{
|
||||
"company_name": args.get("customer_name"),
|
||||
}
|
||||
)
|
||||
contact = frappe.get_doc(values)
|
||||
|
||||
if args.get("email_id"):
|
||||
contact.add_email(args.get("email_id"), is_primary=True)
|
||||
if args.get("mobile_no"):
|
||||
contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True)
|
||||
contact.insert()
|
||||
|
||||
if flags := args.get("flags"):
|
||||
contact.insert(ignore_permissions=flags.get("ignore_permissions"))
|
||||
else:
|
||||
contact.insert()
|
||||
|
||||
return contact
|
||||
|
||||
|
||||
def make_address(args, is_primary_address=1):
|
||||
def make_address(args, is_primary_address=1, is_shipping_address=1):
|
||||
reqd_fields = []
|
||||
for field in ["city", "country"]:
|
||||
if not args.get(field):
|
||||
@@ -717,16 +736,23 @@ def make_address(args, is_primary_address=1):
|
||||
address = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": args.get("name"),
|
||||
"address_title": args.get("customer_name"),
|
||||
"address_line1": args.get("address_line1"),
|
||||
"address_line2": args.get("address_line2"),
|
||||
"city": args.get("city"),
|
||||
"state": args.get("state"),
|
||||
"pincode": args.get("pincode"),
|
||||
"country": args.get("country"),
|
||||
"is_primary_address": is_primary_address,
|
||||
"is_shipping_address": is_shipping_address,
|
||||
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
|
||||
if flags := args.get("flags"):
|
||||
address.insert(ignore_permissions=flags.get("ignore_permissions"))
|
||||
else:
|
||||
address.insert()
|
||||
|
||||
return address
|
||||
|
||||
@@ -747,3 +773,13 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil
|
||||
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]:
|
||||
"""Parse full name into first name, middle name and last name"""
|
||||
names = full_name.split()
|
||||
first_name = names[0]
|
||||
middle_name = " ".join(names[1:-1]) if len(names) > 2 else None
|
||||
last_name = names[-1] if len(names) > 1 else None
|
||||
|
||||
return first_name, middle_name, last_name
|
||||
|
||||
@@ -10,7 +10,11 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.party import get_due_date
|
||||
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
||||
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
|
||||
from erpnext.selling.doctype.customer.customer import (
|
||||
get_credit_limit,
|
||||
get_customer_outstanding,
|
||||
parse_full_name,
|
||||
)
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
|
||||
test_ignore = ["Price List"]
|
||||
@@ -373,6 +377,22 @@ class TestCustomer(FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name")
|
||||
|
||||
def test_parse_full_name(self):
|
||||
first, middle, last = parse_full_name("John")
|
||||
self.assertEqual(first, "John")
|
||||
self.assertEqual(middle, None)
|
||||
self.assertEqual(last, None)
|
||||
|
||||
first, middle, last = parse_full_name("John Doe")
|
||||
self.assertEqual(first, "John")
|
||||
self.assertEqual(middle, None)
|
||||
self.assertEqual(last, "Doe")
|
||||
|
||||
first, middle, last = parse_full_name("John Michael Doe")
|
||||
self.assertEqual(first, "John")
|
||||
self.assertEqual(middle, "Michael")
|
||||
self.assertEqual(last, "Doe")
|
||||
|
||||
|
||||
def get_customer_dict(customer_name):
|
||||
return {
|
||||
|
||||
@@ -73,3 +73,11 @@ def update_page_info(bootinfo):
|
||||
"Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def bootinfo(bootinfo):
|
||||
if bootinfo.get("user") and bootinfo["user"].get("name"):
|
||||
bootinfo["user"]["employee"] = ""
|
||||
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
|
||||
if employee:
|
||||
bootinfo["user"]["employee"] = employee
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
|
||||
def get_leaderboards():
|
||||
@@ -54,12 +54,13 @@ def get_leaderboards():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_customers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Invoice",
|
||||
fields=["customer as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(so_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(so_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "so.transaction_date")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select so.customer as name, {0} as value
|
||||
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
|
||||
ON so.name = so_item.parent
|
||||
where so.docstatus = 1 {1} and so.company = %s
|
||||
group by so.customer
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["customer as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
def get_all_items(date_range, company, field, limit=None):
|
||||
if field in ("available_stock_qty", "available_stock_value"):
|
||||
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
|
||||
return frappe.db.get_all(
|
||||
results = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=["item_code as name", "{0} as value".format(select_field)],
|
||||
group_by="item_code",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
|
||||
return [item for item in results if item["name"] in readable_active_items]
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_purchase_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Purchase Order"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Purchase Order"
|
||||
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select order_item.item_code as name, {0} as value
|
||||
from `tab{1}` sales_order join `tab{1} Item` as order_item
|
||||
on sales_order.name = order_item.parent
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s {2}
|
||||
group by order_item.item_code
|
||||
order by value desc
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, select_doctype, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
child_doctype = f"{select_doctype} Item"
|
||||
return frappe.get_list(
|
||||
select_doctype,
|
||||
fields=[
|
||||
f"`tab{child_doctype}`.item_code as name",
|
||||
f"sum(`tab{child_doctype}`.{select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="value desc",
|
||||
group_by=f"`tab{child_doctype}`.item_code",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_suppliers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
fields=["supplier as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_purchase_amount":
|
||||
select_field = "sum(purchase_order_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(purchase_order_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "purchase_order.modified")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select purchase_order.supplier as name, {0} as value
|
||||
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
|
||||
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
|
||||
where
|
||||
purchase_order.docstatus = 1
|
||||
{1}
|
||||
and purchase_order.company = %s
|
||||
group by purchase_order.supplier
|
||||
order by value DESC
|
||||
limit %s""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
return frappe.get_list(
|
||||
"Purchase Order",
|
||||
fields=["supplier as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(`base_net_total`)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_commission":
|
||||
select_field = "sum(`total_commission`)"
|
||||
select_field = "total_commission"
|
||||
|
||||
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`sales_partner` as name",
|
||||
"{} as value".format(select_field),
|
||||
"sales_partner as name",
|
||||
f"sum({select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="sales_partner",
|
||||
@@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_person(date_range, company, field=None, limit=0):
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [
|
||||
["docstatus", "=", "1"],
|
||||
["company", "=", company],
|
||||
["Sales Team", "sales_person", "is", "set"],
|
||||
]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
|
||||
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
|
||||
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s
|
||||
{date_condition}
|
||||
group by sales_team.sales_person
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
date_condition=date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`tabSales Team`.sales_person as name",
|
||||
"sum(`tabSales Team`.allocated_amount) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="`tabSales Team`.sales_person",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@deprecated
|
||||
def get_date_condition(date_range, field):
|
||||
date_condition = ""
|
||||
if date_range:
|
||||
@@ -236,3 +228,11 @@ def get_date_condition(date_range, field):
|
||||
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
|
||||
)
|
||||
return date_condition
|
||||
|
||||
|
||||
def parse_date_range(date_range):
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
return date_range[0], date_range[1]
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -301,7 +301,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Delivery Note",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -1400,7 +1401,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-04 14:15:28.363184",
|
||||
"modified": "2023-12-18 17:19:39.368239",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -997,7 +997,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""Drop unused return_against index"""
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
|
||||
)
|
||||
frappe.db.sql_ddl(
|
||||
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
|
||||
)
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop unused index")
|
||||
@@ -1332,6 +1332,56 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.reload()
|
||||
self.assertFalse(dn.items[0].target_warehouse)
|
||||
|
||||
def test_sales_return_valuation_for_moving_average(self):
|
||||
item_code = make_item(
|
||||
"_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
|
||||
).name
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=100.0,
|
||||
posting_date=add_days(nowdate(), -5),
|
||||
)
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
self.assertEqual(dn.items[0].incoming_rate, 100.0)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=200.0,
|
||||
posting_date=add_days(nowdate(), -3),
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=300.0,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
)
|
||||
|
||||
dn1 = create_delivery_note(
|
||||
is_return=1,
|
||||
item_code=item_code,
|
||||
return_against=dn.name,
|
||||
qty=-5,
|
||||
rate=500,
|
||||
company=dn.company,
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
do_not_submit=1,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
)
|
||||
|
||||
# (300 * 5) + (200 * 5) = 2500
|
||||
# 2500 / 10 = 250
|
||||
|
||||
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -522,39 +522,25 @@ class TestItem(FrappeTestCase):
|
||||
self.assertEqual(factor, 1.0)
|
||||
|
||||
def test_item_variant_by_manufacturer(self):
|
||||
fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}]
|
||||
set_item_variant_settings(fields)
|
||||
template = make_item(
|
||||
"_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
|
||||
).name
|
||||
|
||||
if frappe.db.exists("Item", "_Test Variant Mfg"):
|
||||
frappe.delete_doc("Item", "_Test Variant Mfg")
|
||||
if frappe.db.exists("Item", "_Test Variant Mfg-1"):
|
||||
frappe.delete_doc("Item", "_Test Variant Mfg-1")
|
||||
if frappe.db.exists("Manufacturer", "MSG1"):
|
||||
frappe.delete_doc("Manufacturer", "MSG1")
|
||||
for manufacturer in ["DFSS", "DASA", "ASAAS"]:
|
||||
if not frappe.db.exists("Manufacturer", manufacturer):
|
||||
m_doc = frappe.new_doc("Manufacturer")
|
||||
m_doc.short_name = manufacturer
|
||||
m_doc.insert()
|
||||
|
||||
template = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Item",
|
||||
item_code="_Test Variant Mfg",
|
||||
has_variant=1,
|
||||
item_group="Products",
|
||||
variant_based_on="Manufacturer",
|
||||
)
|
||||
).insert()
|
||||
self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"}))
|
||||
variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123")
|
||||
|
||||
manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert()
|
||||
item_manufacturer = frappe.db.exists(
|
||||
"Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name}
|
||||
)
|
||||
self.assertTrue(item_manufacturer)
|
||||
|
||||
variant = get_variant(template.name, manufacturer=manufacturer.name)
|
||||
self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
|
||||
self.assertEqual(variant.description, "_Test Variant Mfg")
|
||||
self.assertEqual(variant.manufacturer, "MSG1")
|
||||
variant.insert()
|
||||
|
||||
variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
|
||||
self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
|
||||
self.assertEqual(variant.description, "_Test Variant Mfg")
|
||||
self.assertEqual(variant.manufacturer, "MSG1")
|
||||
self.assertEqual(variant.manufacturer_part_no, "007")
|
||||
frappe.delete_doc("Item Manufacturer", item_manufacturer)
|
||||
|
||||
def test_stock_exists_against_template_item(self):
|
||||
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)
|
||||
|
||||
@@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', {
|
||||
|
||||
get_item_data: function(frm, item, overwrite_warehouse=false) {
|
||||
if (item && !item.item_code) { return; }
|
||||
frm.call({
|
||||
frappe.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_details",
|
||||
child: item,
|
||||
args: {
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
|
||||
@@ -123,7 +123,9 @@ class MaterialRequest(BuyingController):
|
||||
def on_submit(self):
|
||||
self.update_requested_qty_in_production_plan()
|
||||
self.update_requested_qty()
|
||||
if self.material_request_type == "Purchase":
|
||||
if self.material_request_type == "Purchase" and frappe.db.exists(
|
||||
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
|
||||
):
|
||||
self.validate_budget()
|
||||
|
||||
def before_save(self):
|
||||
|
||||
@@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
|
||||
from frappe.utils import cint, floor, flt, today
|
||||
from frappe.utils import ceil, cint, floor, flt, today
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
@@ -605,7 +605,7 @@ def get_available_item_locations_for_serialized_item(
|
||||
.select(sn.name, sn.warehouse)
|
||||
.where((sn.item_code == item_code) & (sn.company == company))
|
||||
.orderby(sn.purchase_date)
|
||||
.limit(cint(required_qty + total_picked_qty))
|
||||
.limit(ceil(required_qty + total_picked_qty))
|
||||
)
|
||||
|
||||
if from_warehouses:
|
||||
@@ -647,7 +647,7 @@ def get_available_item_locations_for_batched_item(
|
||||
.groupby(sle.warehouse, sle.batch_no, sle.item_code)
|
||||
.having(Sum(sle.actual_qty) > 0)
|
||||
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
|
||||
.limit(cint(required_qty + total_picked_qty))
|
||||
.limit(ceil(required_qty + total_picked_qty))
|
||||
)
|
||||
|
||||
if from_warehouses:
|
||||
@@ -680,7 +680,7 @@ def get_available_item_locations_for_serial_and_batched_item(
|
||||
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
|
||||
)
|
||||
.orderby(sn.purchase_date)
|
||||
.limit(cint(location.qty + total_picked_qty))
|
||||
.limit(ceil(location.qty + total_picked_qty))
|
||||
).run(as_dict=True)
|
||||
|
||||
serial_nos = [sn.name for sn in serial_nos]
|
||||
@@ -699,7 +699,7 @@ def get_available_item_locations_for_other_item(
|
||||
.select(bin.warehouse, bin.actual_qty.as_("qty"))
|
||||
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
|
||||
.orderby(bin.creation)
|
||||
.limit(cint(required_qty + total_picked_qty))
|
||||
.limit(ceil(required_qty + total_picked_qty))
|
||||
)
|
||||
|
||||
if from_warehouses:
|
||||
|
||||
@@ -288,7 +288,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Receipt",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_addresses",
|
||||
@@ -1241,7 +1242,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-01 21:00:44.556816",
|
||||
"modified": "2023-12-18 17:26:41.279663",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -1199,10 +1199,6 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
return item_account_wise_cost
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_regional_gl_entries(gl_list, doc):
|
||||
return
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
|
||||
|
||||
@@ -391,7 +392,6 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"net_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": flt(args.discount_amount) or 0.0,
|
||||
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
|
||||
"update_stock": args.get("update_stock")
|
||||
if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"]
|
||||
else 0,
|
||||
@@ -411,6 +411,10 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
}
|
||||
)
|
||||
|
||||
default_supplier = get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults)
|
||||
if default_supplier:
|
||||
out.supplier = default_supplier
|
||||
|
||||
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||
out.update(calculate_service_end_date(args, item))
|
||||
|
||||
@@ -605,6 +609,9 @@ def get_item_tax_template(args, item, out):
|
||||
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if args.get("child_doctype") and item_tax_template:
|
||||
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
|
||||
|
||||
|
||||
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
if out is None:
|
||||
|
||||
@@ -397,7 +397,7 @@ class StockBalanceReport(object):
|
||||
"fieldname": "bal_val",
|
||||
"fieldtype": "Currency",
|
||||
"width": 100,
|
||||
"options": "currency",
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{
|
||||
"label": _("Opening Qty"),
|
||||
@@ -411,7 +411,7 @@ class StockBalanceReport(object):
|
||||
"fieldname": "opening_val",
|
||||
"fieldtype": "Currency",
|
||||
"width": 110,
|
||||
"options": "currency",
|
||||
"options": "Company:company:default_currency",
|
||||
},
|
||||
{
|
||||
"label": _("In Qty"),
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
from erpnext.stock.utils import get_bin
|
||||
@@ -234,6 +235,9 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
|
||||
)
|
||||
|
||||
if status == "Closed":
|
||||
update_po_status("Closed", self.purchase_order)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
||||
@@ -111,13 +111,13 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
self.set_subcontracting_order_status()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_status()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self):
|
||||
|
||||
@@ -655,6 +655,79 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(rm_item.rate, 100)
|
||||
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
|
||||
|
||||
def test_subcontracting_receipt_cancel_with_batch(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
# Step - 1: Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
# Step - 2: Create FG and RM Items
|
||||
fg_item = make_item(
|
||||
properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1}
|
||||
).name
|
||||
rm_item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0})
|
||||
|
||||
# Step - 3: Create BOM for FG Item
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||
for rm_item in bom.items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
bom = bom.name
|
||||
|
||||
# Step - 4: Create PO and SCO
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item Test For Batch 1",
|
||||
"qty": 100,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 100,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
for rm_item in sco.supplied_items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
|
||||
# Step - 5: Inward Raw Materials
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
for rm_item in rm_items:
|
||||
rm_item["rate"] = 100
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
|
||||
# Step - 6: Transfer RM's to Subcontractor
|
||||
se = make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
for item in se.items:
|
||||
self.assertEqual(item.qty, 100)
|
||||
self.assertEqual(item.basic_rate, 100)
|
||||
self.assertEqual(item.amount, item.qty * item.basic_rate)
|
||||
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": fg_item,
|
||||
"batch_id": frappe.generate_hash(length=10),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
# Step - 7: Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].batch_no = batch_doc.batch_id
|
||||
scr.save()
|
||||
scr.submit()
|
||||
scr.load_from_db()
|
||||
|
||||
# Step - 8: Cancel Subcontracting Receipt
|
||||
scr.cancel()
|
||||
self.assertTrue(scr.docstatus == 2)
|
||||
|
||||
|
||||
def make_return_subcontracting_receipt(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", {
|
||||
|
||||
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
|
||||
reason: values.reason,
|
||||
user: frappe.session.user_email
|
||||
user: frappe.session.user_email,
|
||||
doctype: frm.doc.doctype,
|
||||
docname: frm.doc.name,
|
||||
}, () => {
|
||||
reset_sla.enable_primary_action();
|
||||
frm.refresh();
|
||||
|
||||
@@ -739,10 +739,12 @@ def get_response_and_resolution_duration(doc):
|
||||
return priority
|
||||
|
||||
|
||||
def reset_service_level_agreement(doc, reason, user):
|
||||
@frappe.whitelist()
|
||||
def reset_service_level_agreement(doctype: str, docname: str, reason, user):
|
||||
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
|
||||
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
|
||||
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
|
||||
Reference in New Issue
Block a user