mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-24 13:15:03 +00:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e479f918d | ||
|
|
8e8d0ad9aa | ||
|
|
400c78c600 | ||
|
|
f6f2712db0 | ||
|
|
1eee203f7f | ||
|
|
ec337a1c9f | ||
|
|
7577706a9d | ||
|
|
81b5e6c5f1 | ||
|
|
28f052d586 | ||
|
|
2eac618bba | ||
|
|
6f6de13813 | ||
|
|
be1a269f9f | ||
|
|
0d70be56d2 | ||
|
|
50655821c1 | ||
|
|
b254a72d41 | ||
|
|
2770ca1b65 | ||
|
|
0d7cb1a9be | ||
|
|
3abb91b2c4 | ||
|
|
a63b8df867 | ||
|
|
3b9feffc00 | ||
|
|
0c13cb829c | ||
|
|
8aba707b6a | ||
|
|
98bfcc4c75 | ||
|
|
3a668bbe96 | ||
|
|
f2f410093a | ||
|
|
8169c7de0d | ||
|
|
eefac19cc8 | ||
|
|
afc049154d | ||
|
|
4d1ccd9e27 | ||
|
|
c48c8b8ddb | ||
|
|
c2f692a4e4 | ||
|
|
d94b5a318d | ||
|
|
30cb218638 | ||
|
|
d9e0d55b88 | ||
|
|
1e9b4e2403 | ||
|
|
8657ba374a | ||
|
|
3a8b7e9b96 | ||
|
|
22b442d593 | ||
|
|
f7bfbd82dd | ||
|
|
d375164100 | ||
|
|
7fdac62393 | ||
|
|
2184e8ef58 | ||
|
|
87e8dd0d63 | ||
|
|
857843bc29 | ||
|
|
37a18299d0 | ||
|
|
e0c8ff10da | ||
|
|
61b02ec75c | ||
|
|
939b7f7cbf | ||
|
|
77da0da945 | ||
|
|
226d0e0196 | ||
|
|
4114760efd | ||
|
|
23042dfc3c | ||
|
|
9216a852d4 | ||
|
|
6478c80359 | ||
|
|
e18dc5cea3 | ||
|
|
fb5090fd3f | ||
|
|
39aa36e44b | ||
|
|
20a764a4a3 | ||
|
|
24ae45ce84 | ||
|
|
a14e7f0e84 | ||
|
|
7eee573fcb | ||
|
|
0894022695 | ||
|
|
3c717db3fd | ||
|
|
942f34a734 | ||
|
|
4821919695 | ||
|
|
34549b62bd | ||
|
|
d593f81a16 | ||
|
|
e05b23c388 | ||
|
|
d4c131b153 | ||
|
|
9836205725 | ||
|
|
98198d35ea | ||
|
|
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 |
@@ -5,7 +5,7 @@ fail_fast: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
files: "erpnext.*"
|
||||
@@ -15,6 +15,10 @@ repos:
|
||||
args: ['--branch', 'develop']
|
||||
- id: check-merge-conflict
|
||||
- id: check-ast
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.53.0"
|
||||
__version__ = "14.57.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -53,8 +53,13 @@
|
||||
},
|
||||
"II. Forderungen und sonstige Vermögensgegenstände": {
|
||||
"is_group": 1,
|
||||
"Ford. a. Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1400",
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1410",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Durchlaufende Posten": {
|
||||
@@ -180,8 +185,13 @@
|
||||
},
|
||||
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||
"is_group": 1,
|
||||
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1600",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1610",
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -407,13 +407,10 @@
|
||||
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
|
||||
"account_number": "9960"
|
||||
},
|
||||
"Debitoren": {
|
||||
"is_group": 1,
|
||||
"account_number": "10000"
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen": {
|
||||
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "1200",
|
||||
"account_type": "Receivable"
|
||||
"account_type": "Receivable",
|
||||
"is_group": 1
|
||||
},
|
||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "1210"
|
||||
@@ -1138,18 +1135,15 @@
|
||||
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "9964"
|
||||
},
|
||||
"Kreditoren": {
|
||||
"account_number": "70000",
|
||||
"Verb. aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable",
|
||||
"is_group": 1,
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"account_number": "70001",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
}
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen": {
|
||||
"account_number": "3300",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||
"account_number": "3310"
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -138,8 +138,7 @@
|
||||
"label": "Against Voucher Type",
|
||||
"oldfieldname": "against_voucher_type",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher",
|
||||
@@ -158,8 +157,7 @@
|
||||
"label": "Voucher Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
@@ -291,4 +289,4 @@
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -164,6 +164,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
||||
if (!this.frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
this.frm.doc.items.forEach((item) => {
|
||||
this.frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_receipt && item.pr_detail)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
|
||||
|
||||
|
||||
@@ -1660,10 +1660,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
|
||||
@@ -286,7 +286,6 @@
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -894,7 +893,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:26:05.629780",
|
||||
"modified": "2023-12-25 22:00:28.043555",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -2379,10 +2388,6 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -149,11 +149,16 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
},
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
data = execute(filters)
|
||||
|
||||
@@ -181,13 +181,17 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"label": __("Revaluation Journals"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "in_party_currency",
|
||||
"label": __("In Party Currency"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "ignore_accounts",
|
||||
"label": __("Group by Voucher"),
|
||||
"fieldtype": "Check",
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
|
||||
30
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
30
erpnext/accounts/report/accounts_receivable/accounts_receivable.py
Executable file → Normal file
@@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision
|
||||
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
|
||||
# 7. For overpayment against an invoice with payment terms, there will be an additional row
|
||||
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
|
||||
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party
|
||||
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable"
|
||||
# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
|
||||
# 10. This report is based on Payment Ledger Entries
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -84,6 +84,9 @@ class ReceivablePayableReport(object):
|
||||
self.total_row_map = {}
|
||||
self.skip_total_row = 1
|
||||
|
||||
if self.filters.get("in_party_currency"):
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
@@ -145,7 +148,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
|
||||
def get_invoices(self, ple):
|
||||
@@ -224,8 +227,7 @@ class ReceivablePayableReport(object):
|
||||
if not row:
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
@@ -244,8 +246,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
|
||||
@@ -256,8 +262,10 @@ class ReceivablePayableReport(object):
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
if total_row:
|
||||
for field in self.get_currency_fields():
|
||||
total_row[field] += row.get(field, 0.0)
|
||||
total_row["currency"] = row.get("currency", "")
|
||||
|
||||
def append_subtotal_row(self, party):
|
||||
sub_total_row = self.total_row_map.get(party)
|
||||
@@ -318,7 +326,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.get("group_by_party"):
|
||||
self.append_subtotal_row(self.previous_party)
|
||||
if self.data:
|
||||
self.data.append(self.total_row_map.get("Total"))
|
||||
self.data.append(self.total_row_map.get("Total", {}))
|
||||
|
||||
def append_row(self, row):
|
||||
self.allocate_future_payments(row)
|
||||
@@ -449,7 +457,7 @@ class ReceivablePayableReport(object):
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
|
||||
if self.filters.get("party_type") and self.filters.get("party"):
|
||||
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
|
||||
row.currency = row.account_currency
|
||||
else:
|
||||
row.currency = self.company_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,
|
||||
@@ -544,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters.update({"party_account": self.debtors_usd})
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||
expected_data = [100.0, 100.0, self.debtors_usd, si2.currency]
|
||||
row = report[0]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||
@@ -581,6 +616,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
|
||||
@@ -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')")
|
||||
|
||||
|
||||
@@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
|
||||
if filters.get("party"):
|
||||
party = [filters.get("party")]
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
|
||||
)
|
||||
else:
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query = query.where(
|
||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
||||
| (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
| gle.party.isin(party)
|
||||
jv_condition = gle.against.isin(party) | (
|
||||
(gle.voucher_type == "Journal Entry")
|
||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||
)
|
||||
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
"purchase_receipt",
|
||||
"purchase_invoice",
|
||||
"available_for_use_date",
|
||||
"total_asset_cost",
|
||||
"additional_asset_cost",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"asset_quantity",
|
||||
@@ -532,6 +534,22 @@
|
||||
"label": "Capitalized In",
|
||||
"options": "Asset Capitalization",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus > 0",
|
||||
"fieldname": "total_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Asset Cost",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus > 0",
|
||||
"fieldname": "additional_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional Asset Cost",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -565,7 +583,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-20 21:05:45.216899",
|
||||
"modified": "2023-12-21 16:46:20.732869",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -609,4 +627,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class Asset(AccountsController):
|
||||
if self.get("schedules"):
|
||||
self.validate_expected_value_after_useful_life()
|
||||
|
||||
self.total_asset_cost = self.gross_purchase_amount
|
||||
self.status = self.get_status()
|
||||
|
||||
def on_submit(self):
|
||||
@@ -427,6 +428,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 +1392,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
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.increase_asset_value()
|
||||
|
||||
if self.capitalize_repair_cost:
|
||||
self.asset_doc.total_asset_cost += self.repair_cost
|
||||
self.asset_doc.additional_asset_cost += self.repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.check_for_stock_items_and_warehouse()
|
||||
self.decrease_stock_quantity()
|
||||
@@ -68,6 +72,10 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.decrease_asset_value()
|
||||
|
||||
if self.capitalize_repair_cost:
|
||||
self.asset_doc.total_asset_cost -= self.repair_cost
|
||||
self.asset_doc.additional_asset_cost -= self.repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.increase_stock_quantity()
|
||||
if self.get("capitalize_repair_cost"):
|
||||
|
||||
@@ -123,8 +123,7 @@
|
||||
"oldfieldname": "item_code",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_part_no",
|
||||
|
||||
@@ -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,15 @@ 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 or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
):
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
|
||||
@@ -433,8 +433,11 @@ class SubcontractingController(StockController):
|
||||
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
|
||||
self.available_materials[key]["batch_no"][batch_no] = 0
|
||||
|
||||
if abs(qty) > 0 and not new_rm_obj:
|
||||
if new_rm_obj:
|
||||
self.remove(rm_obj)
|
||||
elif abs(qty) > 0:
|
||||
self.__set_consumed_qty(rm_obj, qty)
|
||||
|
||||
else:
|
||||
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
@@ -525,6 +528,10 @@ class SubcontractingController(StockController):
|
||||
(row.item_code, row.get(self.subcontract_data.order_field))
|
||||
] -= row.qty
|
||||
|
||||
def __reset_idx(self):
|
||||
for idx, item in enumerate(self.get(self.raw_material_table)):
|
||||
item.idx = idx + 1
|
||||
|
||||
def __prepare_supplied_items(self):
|
||||
self.initialized_fields()
|
||||
self.__get_subcontract_orders()
|
||||
@@ -532,6 +539,7 @@ class SubcontractingController(StockController):
|
||||
self.get_available_materials()
|
||||
self.__remove_changed_rows()
|
||||
self.__set_supplied_items()
|
||||
self.__reset_idx()
|
||||
|
||||
def __validate_batch_no(self, row, key):
|
||||
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -214,6 +214,7 @@
|
||||
"options": "\nWork Order\nJob Card"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Conversion Rate",
|
||||
@@ -606,7 +607,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 12:47:58.514795",
|
||||
"modified": "2023-12-26 19:34:08.159312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
@@ -645,4 +646,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -305,6 +305,8 @@ frappe.ui.form.on('Production Plan', {
|
||||
frappe.throw(__("Select the Warehouse"));
|
||||
}
|
||||
|
||||
frm.set_value("consider_minimum_order_qty", 0);
|
||||
|
||||
if (frm.doc.ignore_existing_ordered_qty) {
|
||||
frm.events.get_items_for_material_requests(frm);
|
||||
} else {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"material_request_planning",
|
||||
"include_non_stock_items",
|
||||
"include_subcontracted_items",
|
||||
"consider_minimum_order_qty",
|
||||
"include_safety_stock",
|
||||
"ignore_existing_ordered_qty",
|
||||
"column_break_25",
|
||||
@@ -423,13 +424,19 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Sub Assembly Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "consider_minimum_order_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Minimum Order Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 14:08:11.928027",
|
||||
"modified": "2023-12-26 16:31:13.740777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -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:
|
||||
@@ -1131,7 +1135,14 @@ def get_subitems(
|
||||
|
||||
|
||||
def get_material_request_items(
|
||||
row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict
|
||||
doc,
|
||||
row,
|
||||
sales_order,
|
||||
company,
|
||||
ignore_existing_ordered_qty,
|
||||
include_safety_stock,
|
||||
warehouse,
|
||||
bin_dict,
|
||||
):
|
||||
total_qty = row["qty"]
|
||||
|
||||
@@ -1140,8 +1151,14 @@ def get_material_request_items(
|
||||
required_qty = total_qty
|
||||
elif total_qty > bin_dict.get("projected_qty", 0):
|
||||
required_qty = total_qty - bin_dict.get("projected_qty", 0)
|
||||
if required_qty > 0 and required_qty < row["min_order_qty"]:
|
||||
|
||||
if (
|
||||
doc.get("consider_minimum_order_qty")
|
||||
and required_qty > 0
|
||||
and required_qty < row["min_order_qty"]
|
||||
):
|
||||
required_qty = row["min_order_qty"]
|
||||
|
||||
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||
|
||||
if not row["purchase_uom"]:
|
||||
@@ -1479,6 +1496,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
|
||||
if details.qty > 0:
|
||||
items = get_material_request_items(
|
||||
doc,
|
||||
details,
|
||||
sales_order,
|
||||
company,
|
||||
|
||||
@@ -1447,6 +1447,70 @@ 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 test_min_order_qty_in_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}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name
|
||||
|
||||
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, do_not_submit=1)
|
||||
|
||||
pln.for_warehouse = rm_warehouse
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
self.assertEqual(d.get("quantity"), 10.0)
|
||||
|
||||
pln.consider_minimum_order_qty = 1
|
||||
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||
for d in mr_items:
|
||||
self.assertEqual(d.get("quantity"), 1000.0)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -352,5 +352,8 @@ 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.clear_reconciliation_values_from_singles
|
||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
# 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
|
||||
|
||||
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
|
||||
"""
|
||||
)
|
||||
17
erpnext/patches/v14_0/update_total_asset_cost_field.py
Normal file
17
erpnext/patches/v14_0/update_total_asset_cost_field.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run()
|
||||
|
||||
asset_repair_list = frappe.db.get_all(
|
||||
"Asset Repair",
|
||||
filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1},
|
||||
fields=["asset", "repair_cost"],
|
||||
)
|
||||
|
||||
for asset_repair in asset_repair_list:
|
||||
frappe.qb.update(asset).set(
|
||||
asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost
|
||||
).where(asset.name == asset_repair.asset).run()
|
||||
@@ -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};
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("erpnext.selling");
|
||||
|
||||
erpnext.sales_common = {
|
||||
setup_selling_controller:function() {
|
||||
erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales");
|
||||
this.frm.email_field = "contact_email";
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
this.setup_queries();
|
||||
this.frm.set_query('shipping_rule', function() {
|
||||
return {
|
||||
filters: {
|
||||
"shipping_rule_type": "Selling"
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
$.each([["customer", "customer"],
|
||||
["lead", "lead"]],
|
||||
function(i, opts) {
|
||||
if(me.frm.fields_dict[opts[0]])
|
||||
me.frm.set_query(opts[0], erpnext.queries[opts[1]]);
|
||||
});
|
||||
|
||||
me.frm.set_query('contact_person', erpnext.queries.contact_query);
|
||||
me.frm.set_query('customer_address', erpnext.queries.address_query);
|
||||
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
|
||||
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
if(this.frm.fields_dict.selling_price_list) {
|
||||
this.frm.set_query("selling_price_list", function() {
|
||||
return { filters: { selling: 1 } };
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict.tc_name) {
|
||||
this.frm.set_query("tc_name", function() {
|
||||
return { filters: { selling: 1 } };
|
||||
});
|
||||
}
|
||||
|
||||
if(!this.frm.fields_dict["items"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["packed_items"] &&
|
||||
this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) {
|
||||
this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) {
|
||||
return me.set_query_for_batch(doc, cdt, cdn)
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
|
||||
this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
|
||||
return me.set_query_for_item_tax_template(doc, cdt, cdn)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refresh() {
|
||||
super.refresh();
|
||||
|
||||
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
|
||||
|
||||
this.frm.toggle_display("customer_name",
|
||||
(this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer));
|
||||
|
||||
this.toggle_editable_price_list_rate();
|
||||
}
|
||||
|
||||
customer() {
|
||||
var me = this;
|
||||
erpnext.utils.get_party_details(this.frm, null, null, function() {
|
||||
me.apply_price_list();
|
||||
});
|
||||
}
|
||||
|
||||
customer_address() {
|
||||
erpnext.utils.get_address_display(this.frm, "customer_address");
|
||||
erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name");
|
||||
}
|
||||
|
||||
shipping_address_name() {
|
||||
erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address");
|
||||
erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name");
|
||||
}
|
||||
|
||||
dispatch_address_name() {
|
||||
erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address");
|
||||
}
|
||||
|
||||
sales_partner() {
|
||||
this.apply_pricing_rule();
|
||||
}
|
||||
|
||||
campaign() {
|
||||
this.apply_pricing_rule();
|
||||
}
|
||||
|
||||
selling_price_list() {
|
||||
this.apply_price_list();
|
||||
this.set_dynamic_labels();
|
||||
}
|
||||
|
||||
discount_percentage(doc, cdt, cdn) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
item.discount_amount = 0.0;
|
||||
this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage');
|
||||
}
|
||||
|
||||
discount_amount(doc, cdt, cdn) {
|
||||
|
||||
if(doc.name === cdn) {
|
||||
return;
|
||||
}
|
||||
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
item.discount_percentage = 0.0;
|
||||
this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount');
|
||||
}
|
||||
|
||||
commission_rate() {
|
||||
this.calculate_commission();
|
||||
}
|
||||
|
||||
total_commission() {
|
||||
frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
|
||||
|
||||
const { amount_eligible_for_commission } = this.frm.doc;
|
||||
if(!amount_eligible_for_commission) return;
|
||||
|
||||
this.frm.set_value(
|
||||
"commission_rate", flt(
|
||||
this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
allocated_percentage(doc, cdt, cdn) {
|
||||
var sales_person = frappe.get_doc(cdt, cdn);
|
||||
if(sales_person.allocated_percentage) {
|
||||
|
||||
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
|
||||
precision("allocated_percentage", sales_person));
|
||||
|
||||
sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
|
||||
sales_person.allocated_percentage / 100.0,
|
||||
precision("allocated_amount", sales_person));
|
||||
refresh_field(["allocated_amount"], sales_person);
|
||||
|
||||
this.calculate_incentive(sales_person);
|
||||
refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name,
|
||||
sales_person.parentfield);
|
||||
}
|
||||
}
|
||||
|
||||
sales_person(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.calculate_incentive(row);
|
||||
refresh_field("incentives",row.name,row.parentfield);
|
||||
}
|
||||
|
||||
toggle_editable_price_list_rate() {
|
||||
var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
|
||||
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
|
||||
|
||||
if(df && editable_price_list_rate) {
|
||||
const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item");
|
||||
if (!this.frm.fields_dict[parent_field]) return;
|
||||
|
||||
this.frm.fields_dict[parent_field].grid.update_docfield_property(
|
||||
'price_list_rate', 'read_only', 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
calculate_commission() {
|
||||
if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
|
||||
|
||||
if(this.frm.doc.commission_rate > 100) {
|
||||
this.frm.set_value("commission_rate", 100);
|
||||
frappe.throw(`${__(frappe.meta.get_label(
|
||||
this.frm.doc.doctype, "commission_rate", this.frm.doc.name
|
||||
))} ${__("cannot be greater than 100")}`);
|
||||
}
|
||||
|
||||
this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
|
||||
(sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
|
||||
)
|
||||
|
||||
this.frm.doc.total_commission = flt(
|
||||
this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
|
||||
precision("total_commission")
|
||||
);
|
||||
|
||||
refresh_field(["amount_eligible_for_commission", "total_commission"]);
|
||||
}
|
||||
|
||||
calculate_contribution() {
|
||||
var me = this;
|
||||
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
|
||||
frappe.model.round_floats_in(sales_person);
|
||||
if (!sales_person.allocated_percentage) return;
|
||||
|
||||
sales_person.allocated_amount = flt(
|
||||
me.frm.doc.amount_eligible_for_commission
|
||||
* sales_person.allocated_percentage
|
||||
/ 100.0,
|
||||
precision("allocated_amount", sales_person)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
calculate_incentive(row) {
|
||||
if(row.allocated_amount)
|
||||
{
|
||||
row.incentives = flt(
|
||||
row.allocated_amount * row.commission_rate / 100.0,
|
||||
precision("incentives", row));
|
||||
}
|
||||
}
|
||||
|
||||
set_dynamic_labels() {
|
||||
super.set_dynamic_labels();
|
||||
this.set_product_bundle_help(this.frm.doc);
|
||||
}
|
||||
|
||||
set_product_bundle_help(doc) {
|
||||
if(!this.frm.fields_dict.packing_list) return;
|
||||
if ((doc.packed_items || []).length) {
|
||||
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(true);
|
||||
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
var help_msg = "<div class='alert alert-warning'>" +
|
||||
__("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+
|
||||
"</div>";
|
||||
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg;
|
||||
}
|
||||
} else {
|
||||
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(false);
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = '';
|
||||
}
|
||||
}
|
||||
refresh_field('product_bundle_help');
|
||||
}
|
||||
|
||||
company_address() {
|
||||
var me = this;
|
||||
if(this.frm.doc.company_address) {
|
||||
frappe.call({
|
||||
method: "frappe.contacts.doctype.address.address.get_address_display",
|
||||
args: {"address_dict": this.frm.doc.company_address },
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
me.frm.set_value("company_address_display", r.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.frm.set_value("company_address_display", "");
|
||||
}
|
||||
}
|
||||
|
||||
conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) {
|
||||
super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate);
|
||||
}
|
||||
|
||||
qty(doc, cdt, cdn) {
|
||||
super.qty(doc, cdt, cdn);
|
||||
}
|
||||
|
||||
pick_serial_and_batch(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let me = this;
|
||||
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||
.then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
|
||||
|
||||
item.title = item.has_serial_no ?
|
||||
__("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
|
||||
frappe.require(path, function() {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"qty": Math.abs(r.total_qty)
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update_auto_repeat_reference(doc) {
|
||||
if (doc.auto_repeat) {
|
||||
frappe.call({
|
||||
method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
|
||||
args:{
|
||||
docname: doc.auto_repeat,
|
||||
reference:doc.name
|
||||
},
|
||||
callback: function(r){
|
||||
if (r.message=="success") {
|
||||
frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'});
|
||||
} else {
|
||||
frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
project() {
|
||||
let me = this;
|
||||
if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) {
|
||||
if(this.frm.doc.project) {
|
||||
frappe.call({
|
||||
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
|
||||
args: {project: this.frm.doc.project},
|
||||
callback: function(r, rt) {
|
||||
if(!r.exc) {
|
||||
$.each(me.frm.doc["items"] || [], function(i, row) {
|
||||
if(r.message) {
|
||||
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
|
||||
frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message]));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coupon_code() {
|
||||
this.frm.set_value("discount_amount", 0);
|
||||
this.frm.set_value("additional_discount_percentage", 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.pre_sales = {
|
||||
set_as_lost: function(doctype) {
|
||||
frappe.ui.form.on(doctype, {
|
||||
set_as_lost_dialog: function(frm) {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Set as Lost"),
|
||||
fields: [
|
||||
{
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": __("Lost Reasons"),
|
||||
"fieldname": "lost_reason",
|
||||
"options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail',
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": __("Competitors"),
|
||||
"fieldname": "competitors",
|
||||
"options": "Competitor Detail"
|
||||
},
|
||||
{
|
||||
"fieldtype": "Small Text",
|
||||
"label": __("Detailed Reason"),
|
||||
"fieldname": "detailed_reason"
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
let values = dialog.get_values();
|
||||
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'declare_enquiry_lost',
|
||||
args: {
|
||||
'lost_reasons_list': values.lost_reason,
|
||||
'competitors': values.competitors ? values.competitors : [],
|
||||
'detailed_reason': values.detailed_reason
|
||||
},
|
||||
callback: function(r) {
|
||||
dialog.hide();
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Declare Lost')
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,43 @@ 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(
|
||||
{
|
||||
"first_name": args.get("customer_name"),
|
||||
"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 +737,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 +774,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 {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_price_list_rate_for
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -50,6 +50,42 @@ def get_columns(filters=None):
|
||||
]
|
||||
|
||||
|
||||
def fetch_item_prices(
|
||||
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
|
||||
):
|
||||
price_list_map = frappe._dict()
|
||||
ip = qb.DocType("Item Price")
|
||||
and_conditions = []
|
||||
or_conditions = []
|
||||
if items:
|
||||
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
|
||||
and_conditions.append(ip.selling == True)
|
||||
|
||||
or_conditions.append(ip.customer == None)
|
||||
or_conditions.append(ip.price_list == None)
|
||||
|
||||
if customer:
|
||||
or_conditions.append(ip.customer == customer)
|
||||
|
||||
if price_list:
|
||||
or_conditions.append(ip.price_list == price_list)
|
||||
|
||||
if selling_price_list:
|
||||
or_conditions.append(ip.price_list == selling_price_list)
|
||||
|
||||
res = (
|
||||
qb.from_(ip)
|
||||
.select(ip.item_code, ip.price_list, ip.price_list_rate)
|
||||
.where(Criterion.all(and_conditions))
|
||||
.where(Criterion.any(or_conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for x in res:
|
||||
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
|
||||
|
||||
return price_list_map
|
||||
|
||||
|
||||
def get_data(filters=None):
|
||||
data = []
|
||||
customer_details = get_customer_details(filters)
|
||||
@@ -59,9 +95,17 @@ def get_data(filters=None):
|
||||
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
|
||||
)
|
||||
item_stock_map = {item.item_code: item.available for item in item_stock_map}
|
||||
price_list_map = fetch_item_prices(
|
||||
customer_details.customer,
|
||||
customer_details.price_list,
|
||||
customer_details.selling_price_list,
|
||||
items,
|
||||
)
|
||||
|
||||
for item in items:
|
||||
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
|
||||
price_list_rate = price_list_map.get(
|
||||
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
|
||||
)
|
||||
available_stock = item_stock_map.get(item.item_code)
|
||||
|
||||
data.append(
|
||||
|
||||
@@ -200,6 +200,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
item.serial_no = null;
|
||||
}
|
||||
|
||||
if (doc.docstatus === 0 && doc.is_return && !doc.return_against) {
|
||||
item.incoming_rate = 0.0;
|
||||
}
|
||||
|
||||
var has_batch_no;
|
||||
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
|
||||
has_batch_no = r && r.has_batch_no;
|
||||
@@ -428,6 +432,11 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
coupon_code() {
|
||||
this.frm.set_value("discount_amount", 0);
|
||||
this.frm.set_value("additional_discount_percentage", 0);
|
||||
}
|
||||
};
|
||||
|
||||
frappe.ui.form.on(cur_frm.doctype,"project", function(frm) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0.00",
|
||||
|
||||
@@ -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,27 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
UNUSED_INDEXES = [
|
||||
("Delivery Note", ["customer", "is_return", "return_against"]),
|
||||
("Sales Invoice", ["customer", "is_return", "return_against"]),
|
||||
("Purchase Invoice", ["supplier", "is_return", "return_against"]),
|
||||
("Purchase Receipt", ["supplier", "is_return", "return_against"]),
|
||||
]
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype, index_fields in UNUSED_INDEXES:
|
||||
table = f"tab{doctype}"
|
||||
index_name = frappe.db.get_index_name(index_fields)
|
||||
drop_index_if_exists(table, index_name)
|
||||
|
||||
|
||||
def drop_index_if_exists(table: str, index: str):
|
||||
if not frappe.db.has_index(table, index):
|
||||
return
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop index")
|
||||
@@ -1332,6 +1332,109 @@ 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 test_sales_return_valuation_for_moving_average_case2(self):
|
||||
# Make DN return
|
||||
# Make Bakcdated Purchase Receipt and check DN return valuation rate
|
||||
# The rate should be recalculate based on the backdated purchase receipt
|
||||
frappe.flags.print_debug_messages = False
|
||||
item_code = make_item(
|
||||
"_Test Item Sales Return with MA Case2",
|
||||
{"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"},
|
||||
).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,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
rate=500,
|
||||
posting_date=add_days(nowdate(), -4),
|
||||
)
|
||||
|
||||
returned_dn = create_delivery_note(
|
||||
is_return=1,
|
||||
item_code=item_code,
|
||||
return_against=dn.name,
|
||||
qty=-5,
|
||||
rate=500,
|
||||
company=dn.company,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
cost_center="Main - _TC",
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0)
|
||||
|
||||
# Make backdated purchase receipt
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
basic_rate=200.0,
|
||||
posting_date=add_days(nowdate(), -3),
|
||||
)
|
||||
|
||||
returned_dn.reload()
|
||||
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.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:
|
||||
|
||||
@@ -78,6 +78,20 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
if (!frm.doc.is_return) {
|
||||
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
|
||||
if (value) {
|
||||
frm.doc.items.forEach((item) => {
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
"rate", "read_only", (item.purchase_order && item.purchase_order_item)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frm.events.add_custom_buttons(frm);
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -352,7 +352,6 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"print_width": "100px",
|
||||
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -1055,7 +1054,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 16:12:02.364608",
|
||||
"modified": "2023-12-25 22:32:09.801965",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -517,7 +517,12 @@ frappe.ui.form.on('Stock Entry', {
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
["actual_qty", "basic_rate"].forEach((field) => {
|
||||
let fields = ["actual_qty", "basic_rate"];
|
||||
if (frm.doc.purpose == "Material Receipt") {
|
||||
fields = ["actual_qty"];
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
|
||||
});
|
||||
frm.events.calculate_basic_amount(frm, child);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils import cint, flt, get_table_name, getdate
|
||||
from pypika import functions as fn
|
||||
|
||||
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
@@ -12,11 +12,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
SLE_COUNT_LIMIT = 10_000
|
||||
|
||||
|
||||
def _estimate_table_row_count(doctype: str):
|
||||
table = get_table_name(doctype)
|
||||
return cint(
|
||||
frappe.db.sql(
|
||||
f"""select table_rows
|
||||
from information_schema.tables
|
||||
where table_name = '{table}' ;"""
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
sle_count = frappe.db.count("Stock Ledger Entry")
|
||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||
|
||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -16,6 +16,7 @@ from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
from erpnext.stock.utils import (
|
||||
get_incoming_outgoing_rate_for_cancel,
|
||||
get_incoming_rate,
|
||||
get_or_make_bin,
|
||||
get_valuation_method,
|
||||
)
|
||||
@@ -701,7 +702,23 @@ class update_entries_after(object):
|
||||
)
|
||||
|
||||
if self.valuation_method == "Moving Average":
|
||||
rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate)
|
||||
rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": sle.item_code,
|
||||
"warehouse": sle.warehouse,
|
||||
"posting_date": sle.posting_date,
|
||||
"posting_time": sle.posting_time,
|
||||
"qty": sle.actual_qty,
|
||||
"serial_no": sle.get("serial_no"),
|
||||
"batch_no": sle.get("batch_no"),
|
||||
"company": sle.company,
|
||||
"voucher_type": sle.voucher_type,
|
||||
"voucher_no": sle.voucher_no,
|
||||
"allow_zero_valuation": self.allow_zero_rate,
|
||||
"sle": sle.name,
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
rate = get_rate_for_return(
|
||||
sle.voucher_type,
|
||||
|
||||
@@ -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",
|
||||
|
||||
24
erpnext/tests/test_perf.py
Normal file
24
erpnext/tests/test_perf.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
INDEXED_FIELDS = {
|
||||
"Bin": ["item_code"],
|
||||
"GL Entry": ["voucher_type", "against_voucher_type"],
|
||||
"Purchase Order Item": ["item_code"],
|
||||
"Stock Ledger Entry": ["warehouse"],
|
||||
}
|
||||
|
||||
|
||||
class TestPerformance(FrappeTestCase):
|
||||
def test_ensure_indexes(self):
|
||||
# These fields are not explicitly indexed BUT they are prefix in some
|
||||
# other composite index. If those are removed this test should be
|
||||
# updated accordingly.
|
||||
for doctype, fields in INDEXED_FIELDS.items():
|
||||
for field in fields:
|
||||
self.assertTrue(
|
||||
frappe.db.sql(
|
||||
f"""SHOW INDEX FROM `tab{doctype}`
|
||||
WHERE Column_name = "{field}" AND Seq_in_index = 1"""
|
||||
)
|
||||
)
|
||||
@@ -9104,3 +9104,7 @@ Select an item from each set to be used in the Sales Order.,"Wählen Sie aus den
|
||||
Is Alternative,Ist Alternative,
|
||||
Alternative Items,Alternativpositionen,
|
||||
Component Type,Komponententyp,
|
||||
Lost Quotations,Verlorene Angebote,
|
||||
Lost Quotations %,Verlorene Angebote %,
|
||||
Lost Value,Verlorener Wert,
|
||||
Lost Value %,Verlorener Wert %,
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater):
|
||||
"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||
)
|
||||
|
||||
stop_actions = []
|
||||
for ref_dt, ref_dn_field, ref_link_field in ref_details:
|
||||
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
|
||||
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
|
||||
@@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater):
|
||||
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
||||
if action == "Stop":
|
||||
if role_allowed_to_override not in frappe.get_roles():
|
||||
frappe.throw(
|
||||
stop_actions.append(
|
||||
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate
|
||||
)
|
||||
@@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater):
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
if stop_actions:
|
||||
frappe.throw(stop_actions, as_list=True)
|
||||
|
||||
def get_reference_details(self, reference_names, reference_doctype):
|
||||
return frappe._dict(
|
||||
|
||||
Reference in New Issue
Block a user