mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-10 21:21:46 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df4307fef8 | ||
|
|
6039fb75e7 | ||
|
|
edd4e674d3 | ||
|
|
dcdaa02173 | ||
|
|
cd511c5277 | ||
|
|
03bf48032e | ||
|
|
b94a6f2d5c | ||
|
|
3777cae860 | ||
|
|
a67fa6863e | ||
|
|
cd0852f05d | ||
|
|
d30aee5dd6 | ||
|
|
12addb7565 | ||
|
|
95c0dc9b38 | ||
|
|
97b1b7f842 | ||
|
|
27f3981269 | ||
|
|
77f58caa93 | ||
|
|
a93cd02d54 | ||
|
|
dc922abfbe | ||
|
|
5448d5d1cb | ||
|
|
278682f4df | ||
|
|
460ee5d542 | ||
|
|
806ff5bc19 | ||
|
|
c5d72cd934 | ||
|
|
dd0cd8c4f8 | ||
|
|
d5158c6062 | ||
|
|
638c94451e | ||
|
|
8ff5a67b99 | ||
|
|
38249f4170 | ||
|
|
0823de86ad | ||
|
|
e493b11ae3 | ||
|
|
f73737db50 | ||
|
|
8d997c6703 | ||
|
|
797b718e5a | ||
|
|
b22d714103 | ||
|
|
55121a22a5 | ||
|
|
64835b99ec | ||
|
|
4e9faf6ad6 | ||
|
|
24775db97e | ||
|
|
1a24914aed | ||
|
|
56d83d44be | ||
|
|
e940d13068 | ||
|
|
0b9d89b966 | ||
|
|
0a0970e0e7 | ||
|
|
d29686f882 | ||
|
|
dc0bb220ed | ||
|
|
2e8ae4dc30 | ||
|
|
7462de66ce | ||
|
|
c8098e9691 | ||
|
|
193b844bf1 | ||
|
|
e6aaab38b0 | ||
|
|
563e15e1c8 | ||
|
|
ca855e8ab8 | ||
|
|
7a0aef218f | ||
|
|
8f24fd6a60 | ||
|
|
c68afbc715 | ||
|
|
3ed3b214c8 | ||
|
|
26569742fd | ||
|
|
d96c5885a3 | ||
|
|
7af1b5d4ec | ||
|
|
1ad2f06500 | ||
|
|
22dd642413 | ||
|
|
37c90ef873 | ||
|
|
e0cb3ecc0f | ||
|
|
3176bb480f | ||
|
|
17ea958170 | ||
|
|
c16debb8ee | ||
|
|
58feaf1f4a | ||
|
|
dc813ddaec | ||
|
|
09d112a061 | ||
|
|
162ec7d6e8 | ||
|
|
a3fdfba46f | ||
|
|
1b4987c5b4 | ||
|
|
5e0bff3a60 | ||
|
|
6554f192fb | ||
|
|
4ccc426fec | ||
|
|
26c3235bf9 | ||
|
|
68fa55ca02 | ||
|
|
49db19d57a | ||
|
|
953de995bc | ||
|
|
0a36139ef4 | ||
|
|
33eb251727 | ||
|
|
89ba7a22fb | ||
|
|
a853f6add8 | ||
|
|
6c455be6a7 | ||
|
|
53e5fc16d5 | ||
|
|
349caf895b | ||
|
|
2b307ff526 | ||
|
|
f121b33e29 | ||
|
|
0d55a25e6c | ||
|
|
52d5d7a4d3 | ||
|
|
e5e1aadc9a | ||
|
|
3872cdc54a | ||
|
|
c609436283 | ||
|
|
6052e37fc2 | ||
|
|
7225347387 | ||
|
|
5fd68f9fcb | ||
|
|
97e6415afa | ||
|
|
fcd9f89dee | ||
|
|
2bac4b1681 | ||
|
|
8ae9da5484 | ||
|
|
2f2615f9e2 | ||
|
|
8c8b22bc61 | ||
|
|
e0a0cb7b25 | ||
|
|
46ec61f0c9 | ||
|
|
e14d4e839f | ||
|
|
e1f8148129 | ||
|
|
414a91187f | ||
|
|
9315607ee7 | ||
|
|
983c55eca1 | ||
|
|
a90186f9ae | ||
|
|
938888cddd | ||
|
|
7620ae203e | ||
|
|
2e6c8632a1 | ||
|
|
e36880da2d | ||
|
|
8101b51899 | ||
|
|
a070ad786d | ||
|
|
a1ce514cf3 | ||
|
|
32c935bc30 | ||
|
|
8fb3294674 | ||
|
|
b64f5a4e54 | ||
|
|
9864377b47 |
@@ -59,12 +59,14 @@ repos:
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.24.1"
|
||||
__version__ = "15.27.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
|
||||
// hide fields if group
|
||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||
|
||||
// disable fields
|
||||
frm.toggle_enable(["is_group", "company"], false);
|
||||
frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
|
||||
|
||||
if (cint(frm.doc.is_group) == 0) {
|
||||
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
||||
|
||||
@@ -55,8 +55,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"read_only": 1
|
||||
"label": "Account Number"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -72,7 +71,6 @@
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
|
||||
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
|
||||
"General Ledger",
|
||||
"Balance Sheet",
|
||||
"Profit and Loss Statement",
|
||||
"Cash Flow Statement",
|
||||
"Cash Flow",
|
||||
"Accounts Payable",
|
||||
"Accounts Receivable",
|
||||
]) {
|
||||
|
||||
@@ -1525,7 +1525,8 @@
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
"Clients cr\u00e9diteurs": {
|
||||
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
||||
"account_number": "4191"
|
||||
"account_number": "4191",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
||||
"account_number": "4196"
|
||||
@@ -3141,4 +3142,4 @@
|
||||
"account_number": "7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
);
|
||||
|
||||
frm.add_custom_button(__("Auto Reconcile"), function () {
|
||||
if (!frm.doc.bank_account) {
|
||||
frappe.msgprint(__("Please select Bank Account"));
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||
args: {
|
||||
|
||||
@@ -495,12 +495,12 @@ def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
|
||||
@@ -540,14 +540,14 @@ def get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
exact_match=None,
|
||||
common_filters=None,
|
||||
):
|
||||
# get queries to get matching vouchers
|
||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||
@@ -580,15 +580,15 @@ def get_matching_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
exact_match=None,
|
||||
account_from_to=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
common_filters=None,
|
||||
):
|
||||
queries = []
|
||||
currency = get_account_currency(bank_account)
|
||||
|
||||
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if "bank_ac_no" in or_filters:
|
||||
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, format_date, getdate
|
||||
from frappe.utils import add_days, flt, format_date, getdate
|
||||
|
||||
|
||||
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
|
||||
self.validate_child_cost_centers()
|
||||
|
||||
def validate_total_allocation_percentage(self):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
|
||||
@@ -141,7 +141,19 @@ class Dunning(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = [
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 12:35:38.645968",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -127,6 +127,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -1031,6 +1031,17 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
self.currency = row.account_currency
|
||||
self.conversion_rate = row.exchange_rate
|
||||
break
|
||||
else:
|
||||
self.currency = company_currency
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"party",
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -750,6 +751,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -765,6 +767,16 @@
|
||||
"label": "In Words",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "company.reconcile_on_advance_payment_date",
|
||||
"fieldname": "reconcile_on_advance_payment_date",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Reconcile on Advance Payment Date",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -778,7 +790,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-11 11:25:07.366347",
|
||||
"modified": "2024-05-17 10:21:11.199445",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -1249,13 +1249,16 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
if self.reconcile_on_advance_payment_date:
|
||||
posting_date = self.posting_date
|
||||
else:
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
|
||||
@@ -1525,6 +1525,55 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_advance_payment_reconciliation_date(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 1500
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.posting_date = add_days(nowdate(), -1)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
pi = self.create_purchase_invoice(qty=10, rate=100)
|
||||
self.assertNotEqual(pe.posting_date, pi.posting_date)
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
|
||||
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
|
||||
if self.coupon_code:
|
||||
|
||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
pos.reload()
|
||||
|
||||
pos_return1 = make_sales_return(pos.name)
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
bundle_id = frappe.get_doc(
|
||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||
)
|
||||
|
||||
bundle_id.remove(bundle_id.entries[1])
|
||||
bundle_id.save()
|
||||
|
||||
bundle_id.load_from_db()
|
||||
|
||||
serial_no = bundle_id.entries[0].serial_no
|
||||
self.assertEqual(serial_no, serial_nos[0])
|
||||
|
||||
pos_return1.insert()
|
||||
pos_return1.submit()
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||
self.assertEqual(serial_no, serial_nos[1])
|
||||
|
||||
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if isinstance(error_message, list):
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -74,15 +74,21 @@
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_13",
|
||||
"threshold_percentage",
|
||||
"priority",
|
||||
"dynamic_condition_tab",
|
||||
"condition",
|
||||
"column_break_66",
|
||||
"section_break_13",
|
||||
"apply_multiple_pricing_rules",
|
||||
"apply_discount_on_rate",
|
||||
"column_break_66",
|
||||
"threshold_percentage",
|
||||
"validate_pricing_rule_section",
|
||||
"validate_applied_rule",
|
||||
"column_break_texp",
|
||||
"rule_description",
|
||||
"priority_section",
|
||||
"has_priority",
|
||||
"column_break_sayg",
|
||||
"priority",
|
||||
"help_section",
|
||||
"pricing_rule_help",
|
||||
"reference_section",
|
||||
@@ -477,7 +483,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Advanced Settings"
|
||||
},
|
||||
{
|
||||
@@ -487,6 +493,7 @@
|
||||
"label": "Threshold for Suggestion (In Percentage)"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_priority",
|
||||
"description": "Higher the number, higher the priority",
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Select",
|
||||
@@ -513,6 +520,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
||||
"description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
|
||||
"fieldname": "validate_applied_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Applied Rule"
|
||||
@@ -525,7 +533,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "help_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Help Article",
|
||||
"options": "Simple"
|
||||
},
|
||||
{
|
||||
@@ -603,12 +612,42 @@
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_condition_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dynamic Condition"
|
||||
},
|
||||
{
|
||||
"fieldname": "validate_pricing_rule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validate Pricing Rule"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_texp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sayg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this checkbox even if you want to set the zero priority",
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:53:34.887358",
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -27,9 +27,7 @@ class PricingRule(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import (
|
||||
PricingRuleItemCode,
|
||||
)
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
|
||||
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
||||
PricingRuleItemGroup,
|
||||
)
|
||||
@@ -67,6 +65,7 @@ class PricingRule(Document):
|
||||
free_item_rate: DF.Currency
|
||||
free_item_uom: DF.Link | None
|
||||
free_qty: DF.Float
|
||||
has_priority: DF.Check
|
||||
is_cumulative: DF.Check
|
||||
is_recursive: DF.Check
|
||||
item_groups: DF.Table[PricingRuleItemGroup]
|
||||
@@ -156,6 +155,12 @@ class PricingRule(Document):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
def validate_mandatory(self):
|
||||
if self.has_priority and not self.priority:
|
||||
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
|
||||
|
||||
if self.priority and not self.has_priority:
|
||||
self.has_priority = 1
|
||||
|
||||
for apply_on, field in apply_on_dict.items():
|
||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||
|
||||
@@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_pricing_rule_for_transaction_with_condition(self):
|
||||
make_item("PR Transaction Condition")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
make_pricing_rule(
|
||||
selling=1,
|
||||
min_qty=0,
|
||||
price_or_product_discount="Product",
|
||||
apply_on="Transaction",
|
||||
free_item="PR Transaction Condition",
|
||||
free_qty=1,
|
||||
free_item_rate=10,
|
||||
condition="customer=='_Test Customer 1'",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
|
||||
self.assertEqual(len(si.items), 2)
|
||||
self.assertEqual(si.items[1].rate, 10)
|
||||
|
||||
si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
|
||||
self.assertEqual(len(si1.items), 1)
|
||||
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_remove_pricing_rule(self):
|
||||
item = make_item("Water Flask")
|
||||
make_item_price("Water Flask", "_Test Price List", 100)
|
||||
@@ -1157,6 +1181,62 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_priority_of_multiple_pricing_rules(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"has_priority": 1,
|
||||
"priority": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"has_priority": 1,
|
||||
"priority": 3,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_percentage, 20)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
@@ -1185,6 +1265,7 @@ def make_pricing_rule(**args):
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ def get_pricing_rules(args, doc=None):
|
||||
|
||||
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||
if pricing_rules and pricing_rules[0].has_priority:
|
||||
continue
|
||||
|
||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||
break
|
||||
|
||||
@@ -561,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
|
||||
if pricing_rules:
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
|
||||
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
|
||||
|
||||
if not pricing_rules:
|
||||
remove_free_item(doc)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -504,7 +506,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if isinstance(for_filter, str):
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
for_filter = json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
|
||||
@@ -485,10 +485,12 @@ function hide_fields(doc) {
|
||||
|
||||
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
||||
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
if (cur_frm.fields_dict["items"]) {
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
}
|
||||
|
||||
cur_frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_received_but_not_billed = None
|
||||
self.asset_received_but_not_billed = None
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -531,26 +531,45 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif item.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
asset_category_account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account"),
|
||||
account = None
|
||||
if item.pr_detail:
|
||||
if not self.asset_received_but_not_billed:
|
||||
self.asset_received_but_not_billed = self.get_company_default(
|
||||
"asset_received_but_not_billed"
|
||||
)
|
||||
|
||||
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||
arbnb_booked_in_pr = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": item.purchase_receipt,
|
||||
"account": self.asset_received_but_not_billed,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
if arbnb_booked_in_pr:
|
||||
account = self.asset_received_but_not_billed
|
||||
|
||||
if not account:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(
|
||||
form_link, self.company
|
||||
),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
item.expense_account = account
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
|
||||
@@ -707,6 +726,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -1035,10 +1055,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
|
||||
provisional_account, pr_qty, pr_base_rate, pr_rate = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
item.pr_detail,
|
||||
["provisional_expense_account", "qty", "base_rate"],
|
||||
["provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
provisional_account = provisional_account or self.get_company_default(
|
||||
"default_provisional_account"
|
||||
@@ -1072,7 +1092,10 @@ class PurchaseInvoice(BuyingController):
|
||||
self.posting_date,
|
||||
provisional_account,
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
|
||||
item_amount=(
|
||||
(min(item.qty, pr_qty) * pr_rate)
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -55,14 +53,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:27.362567",
|
||||
"modified": "2024-06-03 17:30:37.012593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -71,7 +70,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-10-19 21:59:33.553852",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -99,13 +98,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:35.719727",
|
||||
"modified": "2024-06-03 17:31:04.472279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -114,7 +115,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
@@ -3720,9 +3721,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
|
||||
@@ -870,7 +870,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -926,7 +927,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 15:56:44.828634",
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -936,4 +937,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -161,7 +163,7 @@ def get_linked_payments_for_doc(
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
selections = json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
|
||||
@@ -49,7 +49,6 @@ def get_conditions(filters):
|
||||
|
||||
if filters.account_type:
|
||||
conditions["account_type"] = filters.account_type
|
||||
return conditions
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
|
||||
@@ -75,11 +76,24 @@ def get_data(filters):
|
||||
asset_data = assets_details.get(d.against_voucher)
|
||||
if asset_data:
|
||||
if not asset_data.get("accumulated_depreciation_amount"):
|
||||
asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
|
||||
"opening_accumulated_depreciation"
|
||||
)
|
||||
AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
|
||||
DepreciationSchedule = DocType("Depreciation Schedule")
|
||||
query = (
|
||||
frappe.qb.from_(DepreciationSchedule)
|
||||
.join(AssetDepreciationSchedule)
|
||||
.on(DepreciationSchedule.parent == AssetDepreciationSchedule.name)
|
||||
.select(DepreciationSchedule.accumulated_depreciation_amount)
|
||||
.where(
|
||||
(AssetDepreciationSchedule.asset == d.against_voucher)
|
||||
& (DepreciationSchedule.parenttype == "Asset Depreciation Schedule")
|
||||
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||
|
||||
else:
|
||||
asset_data.accumulated_depreciation_amount += d.debit
|
||||
asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit
|
||||
|
||||
row = frappe._dict(asset_data)
|
||||
row.update(
|
||||
|
||||
@@ -219,7 +219,8 @@ def get_conditions(filters):
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
if filters.account:
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@@ -329,7 +330,7 @@ def get_accounts_with_children(accounts):
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
return list(set(all_accounts)) if all_accounts else None
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
@@ -481,6 +482,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
elif group_by_voucher_consolidated:
|
||||
keylist = [
|
||||
gle.get("posting_date"),
|
||||
gle.get("voucher_type"),
|
||||
gle.get("voucher_no"),
|
||||
gle.get("account"),
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for d in entries:
|
||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
||||
|
||||
if d.reference_type == "Purchase Invoice":
|
||||
payment_amount = flt(d.debit) or -1 * flt(d.credit)
|
||||
else:
|
||||
payment_amount = flt(d.credit) or -1 * flt(d.debit)
|
||||
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||
payment_amount = d.amount
|
||||
|
||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
||||
|
||||
if d.against_voucher:
|
||||
if d.against_voucher_no:
|
||||
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||
|
||||
row = [
|
||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
||||
d.party_type,
|
||||
d.party,
|
||||
d.posting_date,
|
||||
d.against_voucher,
|
||||
d.against_voucher_no,
|
||||
invoice.posting_date,
|
||||
invoice.due_date,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.amount,
|
||||
d.remarks,
|
||||
d.age,
|
||||
d.range1,
|
||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
},
|
||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = []
|
||||
|
||||
if not filters.party_type:
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
filters.party_type = "Supplier"
|
||||
else:
|
||||
filters.party_type = "Customer"
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("party_type=%(party_type)s")
|
||||
conditions.append(ple.delinked.eq(0))
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
conditions.append(ple.party_type.eq("Supplier"))
|
||||
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||
else:
|
||||
conditions.append(ple.party_type.eq("Customer"))
|
||||
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||
|
||||
if filters.party:
|
||||
conditions.append("party=%(party)s")
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("against_voucher_type=%(reference_type)s")
|
||||
filters["reference_type"] = (
|
||||
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
|
||||
)
|
||||
conditions.append(ple.party.eq(filters.party))
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append("posting_date >= %(from_date)s")
|
||||
conditions.append(ple.posting_date.gte(filters.get("from_date")))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append("posting_date <= %(to_date)s")
|
||||
conditions.append(ple.posting_date.lte(filters.get("to_date")))
|
||||
|
||||
return "and " + " and ".join(conditions) if conditions else ""
|
||||
if filters.get("company"):
|
||||
conditions.append(ple.company.eq(filters.get("company")))
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
||||
""".format(get_conditions(filters)),
|
||||
filters,
|
||||
as_dict=1,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
Abs(ple.amount).as_("amount"),
|
||||
ple.remarks,
|
||||
ple.against_voucher_no,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
|
||||
|
||||
def get_invoice_posting_date_map(filters):
|
||||
invoice_details = {}
|
||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
||||
dt = (
|
||||
qb.DocType("Sales Invoice")
|
||||
if filters.get("payment_type") == _("Incoming")
|
||||
else qb.DocType("Purchase Invoice")
|
||||
)
|
||||
res = (
|
||||
qb.from_(dt)
|
||||
.select(dt.name, dt.posting_date, dt.due_date)
|
||||
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
|
||||
.run(as_dict=1)
|
||||
)
|
||||
for t in res:
|
||||
invoice_details[t.name] = t
|
||||
|
||||
return invoice_details
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
|
||||
frappe.query_reports["Purchase Invoice Trends"] = {
|
||||
filters: erpnext.get_purchase_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Purchase Invoice Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/sales_trends_filters.js", function () {
|
||||
frappe.query_reports["Sales Invoice Trends"] = {
|
||||
filters: erpnext.get_sales_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Sales Invoice Trends"] = $.extend({}, erpnext.sales_trends_filters);
|
||||
|
||||
@@ -56,7 +56,7 @@ def get_fiscal_year(
|
||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
|
||||
):
|
||||
if isinstance(boolean, str):
|
||||
boolean = frappe.json.loads(boolean)
|
||||
boolean = loads(boolean)
|
||||
|
||||
fiscal_years = get_fiscal_years(
|
||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
|
||||
|
||||
@@ -1140,6 +1140,8 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
|
||||
for row in new_asset.get("finance_books"):
|
||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
||||
if not current_asset_depr_schedule_doc:
|
||||
continue
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
|
||||
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
|
||||
|
||||
@@ -363,6 +363,16 @@ class AssetDepreciationSchedule(Document):
|
||||
row.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations."
|
||||
).format(
|
||||
frappe.bold(asset_doc.gross_purchase_amount),
|
||||
frappe.bold(row.total_number_of_depreciations),
|
||||
frappe.bold(row.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
|
||||
@@ -159,8 +159,9 @@ def prepare_chart_data(data, filters):
|
||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||
filters_filter_based_on = "Date Range"
|
||||
date_field = "purchase_date"
|
||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filtered_data = [d for d in data if d.get(date_field)]
|
||||
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
else:
|
||||
filters_filter_based_on = filters.filter_based_on
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
|
||||
@@ -902,12 +902,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
target_doc.populate_items_table()
|
||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
|
||||
@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||
args: {
|
||||
doctype: "Supplier",
|
||||
tag: args.tag,
|
||||
tag: "%" + args.tag + "%",
|
||||
},
|
||||
callback: load_suppliers,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
|
||||
frappe.query_reports["Purchase Order Trends"] = {
|
||||
filters: erpnext.get_purchase_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||
|
||||
@@ -2183,10 +2183,10 @@ class AccountsController(TransactionBase):
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
|
||||
@@ -422,6 +422,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
& (stock_ledger_entry.batch_no.isnotnull())
|
||||
)
|
||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||
.having(Sum(stock_ledger_entry.actual_qty) > 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
@@ -472,6 +473,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||
)
|
||||
.groupby(bundle.batch_no, bundle.warehouse)
|
||||
.having(Sum(bundle.qty) > 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt, format_datetime, get_datetime
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.rejected_warehouse = ""
|
||||
target_doc.warehouse = source_doc.rejected_warehouse
|
||||
target_doc.received_qty = target_doc.qty
|
||||
target_doc.return_qty_from_rejected_warehouse = 1
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if source_doc.item_code:
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
and not source_doc.use_serial_batch_fields
|
||||
):
|
||||
target_doc.set("use_serial_batch_fields", 1)
|
||||
|
||||
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
if not target_doc.get("use_serial_batch_fields"):
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if not target_doc.get(qty_field):
|
||||
continue
|
||||
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
elif target_doc.get("use_serial_batch_fields"):
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
|
||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -839,3 +841,229 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
|
||||
batches.update(get_batches_from_bundle(ids))
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
|
||||
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not available_dict:
|
||||
frappe.throw(_("No Serial / Batches are available for return"))
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
],
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
for row in batch_serial_nos:
|
||||
key = row.voucher_detail_no
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
del filters["serial_and_batch_bundle"]
|
||||
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
|
||||
pluck_field = "rejected_serial_and_batch_bundle"
|
||||
|
||||
_bundle_ids = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
if not is_rejected:
|
||||
_bundle_ids.extend(
|
||||
frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if is_rejected:
|
||||
if d.get("return_qty_from_rejected_warehouse"):
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
for serial_no, sn_qty in data.serial_nos.items():
|
||||
if sn_qty != 0:
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
else:
|
||||
filterd_serial_batch["serial_nos"] = available_serial_nos
|
||||
|
||||
elif data.batches:
|
||||
for batch_no, batch_qty in data.batches.items():
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
batch_qty = batch_qty * -1
|
||||
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
warehouse,
|
||||
)
|
||||
|
||||
if batch_qty <= 0:
|
||||
frappe.throw(
|
||||
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
|
||||
title=_("Batch Not Available for Return"),
|
||||
)
|
||||
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
if batch_qty > qty:
|
||||
filterd_serial_batch["batches"][batch_no] = qty
|
||||
qty = 0
|
||||
else:
|
||||
filterd_serial_batch["batches"][batch_no] += batch_qty
|
||||
qty -= batch_qty
|
||||
|
||||
return filterd_serial_batch
|
||||
|
||||
|
||||
def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
return get_batch_qty(
|
||||
batch_no,
|
||||
warehouse,
|
||||
posting_date=parent_doc.posting_date,
|
||||
posting_time=parent_doc.posting_time,
|
||||
for_stock_levels=True,
|
||||
)
|
||||
|
||||
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
type_of_transaction = "Outward"
|
||||
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"item_code": child_doc.item_code,
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
"qty": child_doc.qty,
|
||||
"company": parent_doc.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
return cls_obj.name
|
||||
|
||||
|
||||
def get_available_serial_nos(serial_nos, warehouse):
|
||||
return frappe.get_all(
|
||||
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
|
||||
)
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.controllers.sales_and_purchase_return import (
|
||||
available_serial_batch_for_return,
|
||||
filter_serial_batches,
|
||||
make_serial_batch_bundle_for_return,
|
||||
)
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
@@ -217,6 +222,125 @@ class StockController(AccountsController):
|
||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||
if not self.get("is_return"):
|
||||
return
|
||||
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
|
||||
self.make_bundle_for_non_rejected_qty(table_name)
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
self.make_bundle_for_rejected_qty(table_name)
|
||||
|
||||
def make_bundle_for_rejected_qty(self, table_name=None):
|
||||
field, reference_ids = self.get_reference_ids(
|
||||
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(
|
||||
field, child_doctype, reference_ids, is_rejected=True
|
||||
)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
qty_field = "rejected_qty"
|
||||
warehouse_field = "rejected_warehouse"
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
qty_field = "qty"
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
data = filter_serial_batches(
|
||||
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||
)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def make_bundle_for_non_rejected_qty(self, table_name):
|
||||
field, reference_ids = self.get_reference_ids(table_name)
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Delivery Note": "dn_detail",
|
||||
"Purchase Receipt": "purchase_receipt_item",
|
||||
"Purchase Invoice": "purchase_invoice_item",
|
||||
"POS Invoice": "pos_invoice_item",
|
||||
}.get(self.doctype)
|
||||
|
||||
if not bundle_field:
|
||||
bundle_field = "serial_and_batch_bundle"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
reference_ids = []
|
||||
|
||||
for row in self.get(table_name):
|
||||
if not self.is_serial_batch_item(row.item_code):
|
||||
continue
|
||||
|
||||
if (
|
||||
row.get(field)
|
||||
and (
|
||||
qty_field == "qty"
|
||||
and not row.get("return_qty_from_rejected_warehouse")
|
||||
or qty_field == "rejected_qty"
|
||||
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
|
||||
)
|
||||
and not row.get("use_serial_batch_fields")
|
||||
and not row.get(bundle_field)
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
def is_serial_batch_item(self, item_code) -> bool:
|
||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if item_details.has_serial_no or item_details.has_batch_no:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -611,35 +735,16 @@ class StockController(AccountsController):
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||
):
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
|
||||
|
||||
if not type_of_transaction:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = warehouse
|
||||
bundle_doc.type_of_transaction = type_of_transaction
|
||||
bundle_doc.voucher_type = self.doctype
|
||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
return make_bundle_for_material_transfer(
|
||||
is_new=self.is_new(),
|
||||
docstatus=self.docstatus,
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||
warehouse=warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
sl_dict = frappe._dict(
|
||||
@@ -1557,3 +1662,38 @@ def create_item_wise_repost_entries(
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
return repost_entries
|
||||
|
||||
|
||||
def make_bundle_for_material_transfer(**kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
|
||||
|
||||
if not kwargs.type_of_transaction:
|
||||
kwargs.type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = kwargs.warehouse
|
||||
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||
bundle_doc.voucher_type = kwargs.voucher_type
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if kwargs.type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = kwargs.warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
|
||||
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.serial_no:
|
||||
if row.serial_no and not consumed_bundles.serial_nos:
|
||||
self.available_materials[key]["serial_no"] = list(
|
||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.batch_no:
|
||||
if row.batch_no and not consumed_bundles.batch_nos:
|
||||
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
||||
|
||||
def get_available_materials(self):
|
||||
|
||||
@@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
self.title = self.customer_name
|
||||
|
||||
self.calculate_totals()
|
||||
|
||||
def on_update(self):
|
||||
self.update_prospect()
|
||||
|
||||
def map_fields(self):
|
||||
|
||||
@@ -591,13 +591,13 @@
|
||||
"default": "0",
|
||||
"fieldname": "fg_based_operating_cost",
|
||||
"fieldtype": "Check",
|
||||
"label": "FG based Operating Cost"
|
||||
"label": "Finished Goods based Operating Cost"
|
||||
},
|
||||
{
|
||||
"depends_on": "fg_based_operating_cost",
|
||||
"fieldname": "fg_based_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "FG Based Operating Cost Section"
|
||||
"label": "Finished Goods Based Operating Cost"
|
||||
},
|
||||
{
|
||||
"depends_on": "fg_based_operating_cost",
|
||||
@@ -637,7 +637,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-02 16:22:47.518411",
|
||||
"modified": "2024-06-03 16:24:47.518411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -80,6 +80,18 @@ class BOMCreator(Document):
|
||||
if row.is_expandable and row.item_code == self.item_code:
|
||||
frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
|
||||
|
||||
if not row.parent_row_no and row.fg_item and row.fg_item != self.item_code:
|
||||
frappe.throw(
|
||||
_("At row {0}: set Parent Row No for item {1}").format(row.idx, row.item_code),
|
||||
title=_("Set Parent Row No in Items Table"),
|
||||
)
|
||||
|
||||
elif row.parent_row_no and row.fg_item == self.item_code:
|
||||
frappe.throw(
|
||||
_("At row {0}: Parent Row No cannot be set for item {1}").format(row.idx, row.item_code),
|
||||
title=_("Remove Parent Row No in Items Table"),
|
||||
)
|
||||
|
||||
def set_status(self, save=False):
|
||||
self.status = {
|
||||
0: "Draft",
|
||||
@@ -410,6 +422,10 @@ def add_sub_assembly(**kwargs):
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
||||
if parent_row_no:
|
||||
parent_row_no = parent_row_no[0]
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"fieldname": "fg_item",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "FG Item",
|
||||
"label": "Finished Goods Item",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -203,7 +203,7 @@
|
||||
{
|
||||
"fieldname": "fg_reference_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "FG Reference",
|
||||
"label": "Finished Goods Reference",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -230,7 +230,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-16 13:34:06.321061",
|
||||
"modified": "2024-06-03 18:45:24.339532",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Creator Item",
|
||||
|
||||
@@ -214,7 +214,11 @@ class JobCard(Document):
|
||||
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
|
||||
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
|
||||
|
||||
data = self.get_overlap_for(d)
|
||||
open_job_cards = []
|
||||
if d.get("employee"):
|
||||
open_job_cards = self.get_open_job_cards(d.get("employee"))
|
||||
|
||||
data = self.get_overlap_for(d, open_job_cards=open_job_cards)
|
||||
if data:
|
||||
frappe.throw(
|
||||
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
|
||||
@@ -235,12 +239,12 @@ class JobCard(Document):
|
||||
for row in self.sub_operations:
|
||||
self.total_completed_qty += row.completed_qty
|
||||
|
||||
def get_overlap_for(self, args):
|
||||
def get_overlap_for(self, args, open_job_cards=None):
|
||||
time_logs = []
|
||||
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
||||
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", open_job_cards=open_job_cards))
|
||||
|
||||
if not time_logs:
|
||||
return {}
|
||||
@@ -304,7 +308,7 @@ class JobCard(Document):
|
||||
return True
|
||||
return overlap
|
||||
|
||||
def get_time_logs(self, args, doctype):
|
||||
def get_time_logs(self, args, doctype, open_job_cards=None):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType(doctype)
|
||||
|
||||
@@ -341,8 +345,14 @@ class JobCard(Document):
|
||||
if self.workstation:
|
||||
query = query.where(jc.workstation == self.workstation)
|
||||
|
||||
if args.get("employee") and doctype == "Job Card Time Log":
|
||||
query = query.where(jctl.employee == args.get("employee"))
|
||||
if args.get("employee"):
|
||||
if not open_job_cards and doctype == "Job Card Scheduled Time":
|
||||
return []
|
||||
|
||||
if doctype == "Job Card Time Log":
|
||||
query = query.where(jctl.employee == args.get("employee"))
|
||||
else:
|
||||
query = query.where(jc.name.isin(open_job_cards))
|
||||
|
||||
if doctype != "Job Card Time Log":
|
||||
query = query.where(jc.total_time_in_mins == 0)
|
||||
@@ -351,6 +361,27 @@ class JobCard(Document):
|
||||
|
||||
return time_logs
|
||||
|
||||
def get_open_job_cards(self, employee):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(jc)
|
||||
.left_join(jctl)
|
||||
.on(jc.name == jctl.parent)
|
||||
.select(jc.name)
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (jc.workstation == self.workstation)
|
||||
& (jctl.employee == employee)
|
||||
& (jc.docstatus < 1)
|
||||
& (jc.name != self.name)
|
||||
)
|
||||
)
|
||||
|
||||
jobs = query.run(as_dict=True)
|
||||
return [job.get("name") for job in jobs] if jobs else []
|
||||
|
||||
def get_workstation_based_on_available_slot(self, existing_time_logs) -> dict:
|
||||
workstations = get_workstations(self.workstation_type)
|
||||
if workstations:
|
||||
|
||||
@@ -42,8 +42,7 @@
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Qty",
|
||||
"reqd": 1
|
||||
"label": "Completed Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
@@ -64,7 +63,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-23 14:30:00.970916",
|
||||
"modified": "2024-05-21 12:40:55.765860",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Time Log",
|
||||
@@ -74,4 +73,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "FG Warehouse",
|
||||
"label": "Finished Goods Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -220,7 +220,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-27 13:24:43.571844",
|
||||
"modified": "2024-06-03 13:10:20.252166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
|
||||
@@ -21,7 +21,8 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
exploded_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
filters={"parent": bom},
|
||||
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
|
||||
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom", "idx"],
|
||||
order_by="idx ASC",
|
||||
)
|
||||
|
||||
for item in exploded_items:
|
||||
|
||||
@@ -93,4 +93,11 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
|
||||
},
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (column.fieldname === "item_code" && value.includes("Total Quantity")) {
|
||||
value = "<strong>" + value + "</strong>";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -144,7 +144,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
|
||||
total_row = {"item_code": _("Total Quantity")}
|
||||
|
||||
for value in self.data:
|
||||
for period in self.period_list:
|
||||
|
||||
@@ -102,7 +102,12 @@ def get_columns() -> Columns:
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
|
||||
{
|
||||
"label": _("Finished Goods Value"),
|
||||
"fieldname": "total_fg_value",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{
|
||||
"label": _("Raw Material Value"),
|
||||
"fieldname": "total_rm_value",
|
||||
|
||||
@@ -364,3 +364,4 @@ erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
|
||||
@@ -13,8 +13,9 @@ def execute():
|
||||
for d in accounting_dimensions:
|
||||
doctype = "Asset Repair"
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
docfield = frappe.db.get_value("DocField", {"parent": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
if field or docfield:
|
||||
continue
|
||||
|
||||
df = {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
pr_table = frappe.qb.DocType("Pricing Rule")
|
||||
(
|
||||
frappe.qb.update(pr_table)
|
||||
.set(pr_table.has_priority, 1)
|
||||
.where((pr_table.priority.isnotnull()) & (pr_table.priority != ""))
|
||||
).run()
|
||||
@@ -55,6 +55,14 @@ frappe.ui.form.on("Project", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -83,7 +83,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
this.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
calculate_discount_amount() {
|
||||
@@ -841,7 +841,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
this.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||
|
||||
@@ -2295,6 +2295,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
if (doc.is_return) {
|
||||
filters["is_return"] = 1;
|
||||
if (["Sales Invoice", "Delivery Note"].includes(doc.doctype)) {
|
||||
filters["is_inward"] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.warehouse) filters["warehouse"] = item.warehouse;
|
||||
|
||||
@@ -34,5 +34,7 @@ import "./utils/sales_common.js";
|
||||
import "./controllers/buying.js";
|
||||
import "./utils/demo.js";
|
||||
import "./financial_statements.js";
|
||||
import "./sales_trends_filters.js";
|
||||
import "./purchase_trends_filters.js";
|
||||
|
||||
// import { sum } from 'frappe/public/utils/util.js'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
erpnext.get_purchase_trends_filters = function () {
|
||||
return [
|
||||
erpnext.purchase_trends_filters = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
@@ -63,5 +63,5 @@ erpnext.get_purchase_trends_filters = function () {
|
||||
options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }],
|
||||
default: "",
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
erpnext.get_sales_trends_filters = function () {
|
||||
return [
|
||||
erpnext.sales_trends_filters = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "period",
|
||||
label: __("Period"),
|
||||
@@ -53,5 +53,5 @@ erpnext.get_sales_trends_filters = function () {
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1183,4 +1183,39 @@ $.extend(erpnext.stock.utils, {
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
||||
barcode_scanner.scan_api_call(child_row.barcode, callback);
|
||||
},
|
||||
|
||||
get_serial_range(range_string, separator) {
|
||||
/* Return an array of serial numbers generated from a range string.
|
||||
|
||||
Examples (using separator "::"):
|
||||
- "1::5" => ["1", "2", "3", "4", "5"]
|
||||
- "SN0009::12" => ["SN0009", "SN0010", "SN0011", "SN0012"]
|
||||
- "ABC//05::8" => ["ABC//05", "ABC//06", "ABC//07", "ABC//08"]
|
||||
*/
|
||||
if (!range_string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [start_str, end_str] = range_string.trim().split(separator);
|
||||
|
||||
if (!start_str || !end_str) {
|
||||
return;
|
||||
}
|
||||
|
||||
const end_int = parseInt(end_str);
|
||||
const length_difference = start_str.length - end_str.length;
|
||||
const start_int = parseInt(start_str.substring(length_difference));
|
||||
|
||||
if (isNaN(start_int) || isNaN(end_int)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serial_numbers = Array(end_int - start_int + 1)
|
||||
.fill(1)
|
||||
.map((x, y) => x + y)
|
||||
.map((x) => x + start_int - 1);
|
||||
return serial_numbers.map((val) => {
|
||||
return start_str.substring(0, length_difference) + val.toString().padStart(end_str.length, "0");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -206,6 +206,16 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
label: __("{0} {1} Manually", [primary_label, label]),
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Enter Serial No Range"),
|
||||
fieldname: "serial_no_range",
|
||||
depends_on: "eval:doc.import_using_csv_file === 0",
|
||||
description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'),
|
||||
onchange: () => {
|
||||
this.set_serial_nos_from_range();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
label: __("Enter Serial Nos"),
|
||||
@@ -255,6 +265,20 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
return fields;
|
||||
}
|
||||
|
||||
set_serial_nos_from_range() {
|
||||
const serial_no_range = this.dialog.get_value("serial_no_range");
|
||||
|
||||
if (!serial_no_range) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serial_nos = erpnext.stock.utils.get_serial_range(serial_no_range, "::");
|
||||
|
||||
if (serial_nos) {
|
||||
this.dialog.set_value("upload_serial_nos", serial_nos.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
create_serial_nos() {
|
||||
let { upload_serial_nos } = this.dialog.get_values();
|
||||
|
||||
@@ -589,7 +613,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
}
|
||||
|
||||
render_data() {
|
||||
if (this.bundle) {
|
||||
if (this.bundle || this.frm.doc.is_return) {
|
||||
frappe
|
||||
.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers",
|
||||
@@ -597,6 +621,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
item_code: this.item.item_code,
|
||||
name: this.bundle,
|
||||
voucher_no: !this.frm.is_new() ? this.item.parent : "",
|
||||
child_row: this.frm.doc.is_return ? this.item : "",
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
@@ -321,7 +323,7 @@ class TestCustomer(FrappeTestCase):
|
||||
frappe.ValidationError,
|
||||
update_child_qty_rate,
|
||||
so.doctype,
|
||||
frappe.json.dumps([modified_item]),
|
||||
json.dumps([modified_item]),
|
||||
so.name,
|
||||
)
|
||||
|
||||
|
||||
@@ -9,5 +9,13 @@ frappe.ui.form.on("Product Bundle", {
|
||||
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("item_code", "items", () => {
|
||||
return {
|
||||
filters: {
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -867,6 +867,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
fields: fields,
|
||||
primary_action: function () {
|
||||
var data = { items: d.fields_dict.items.grid.get_selected_children() };
|
||||
if (!data) {
|
||||
frappe.throw(__("Please select items"));
|
||||
}
|
||||
me.frm.call({
|
||||
method: "make_work_orders",
|
||||
args: {
|
||||
|
||||
@@ -1139,7 +1139,8 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Inter Company Order Reference",
|
||||
"options": "Purchase Order",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -1645,7 +1646,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-29 16:27:41.539613",
|
||||
"modified": "2024-05-23 16:35:54.905804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -1224,11 +1224,19 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
target.discount_amount = 0.0
|
||||
target.inter_company_order_reference = ""
|
||||
target.shipping_rule = ""
|
||||
target.tc_name = ""
|
||||
target.terms = ""
|
||||
target.payment_terms_template = ""
|
||||
target.payment_schedule = []
|
||||
|
||||
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
|
||||
if default_price_list:
|
||||
target.buying_price_list = default_price_list
|
||||
|
||||
default_payment_terms = frappe.get_value("Supplier", supplier, "payment_terms")
|
||||
if default_payment_terms:
|
||||
target.payment_terms_template = default_payment_terms
|
||||
|
||||
if any(item.delivered_by_supplier == 1 for item in source.items):
|
||||
if source.shipping_address_name:
|
||||
target.shipping_address = source.shipping_address_name
|
||||
@@ -1280,7 +1288,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address",
|
||||
"terms",
|
||||
],
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
@@ -1348,6 +1355,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.discount_amount = 0.0
|
||||
target.inter_company_order_reference = ""
|
||||
target.shipping_rule = ""
|
||||
target.tc_name = ""
|
||||
target.terms = ""
|
||||
target.payment_terms_template = ""
|
||||
target.payment_schedule = []
|
||||
|
||||
if is_drop_ship_order(target):
|
||||
target.customer = source.customer
|
||||
@@ -1383,7 +1394,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address",
|
||||
"terms",
|
||||
],
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
|
||||
@@ -684,7 +684,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
const is_stock_item = resp[1];
|
||||
|
||||
frappe.dom.unfreeze();
|
||||
const bold_uom = item_row.stock_uom.bold();
|
||||
const bold_uom = item_row.uom.bold();
|
||||
const bold_item_code = item_row.item_code.bold();
|
||||
const bold_warehouse = warehouse.bold();
|
||||
const bold_available_qty = available_qty.toString().bold();
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/sales_trends_filters.js", function () {
|
||||
frappe.query_reports["Quotation Trends"] = {
|
||||
filters: erpnext.get_sales_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Quotation Trends"] = $.extend({}, erpnext.sales_trends_filters);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/sales_trends_filters.js", function () {
|
||||
frappe.query_reports["Sales Order Trends"] = {
|
||||
filters: erpnext.get_sales_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Sales Order Trends"] = $.extend({}, erpnext.sales_trends_filters);
|
||||
|
||||
@@ -164,9 +164,10 @@ def prepare_data(
|
||||
rows = {}
|
||||
|
||||
target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount"
|
||||
|
||||
qty_or_amount_field = "stock_qty" if filters.get("target_on") == "Quantity" else "base_net_amount"
|
||||
|
||||
item_group_parent_child_map = get_item_group_parent_child_map()
|
||||
|
||||
for d in sales_users_data:
|
||||
key = (d.parent, d.item_group)
|
||||
dist_data = get_periodwise_distribution_data(d.distribution_id, period_list, filters.get("period"))
|
||||
@@ -191,7 +192,11 @@ def prepare_data(
|
||||
r.get(sales_field) == d.parent
|
||||
and period.from_date <= r.get(date_field)
|
||||
and r.get(date_field) <= period.to_date
|
||||
and (not sales_user_wise_item_groups.get(d.parent) or r.item_group == d.item_group)
|
||||
and (
|
||||
not sales_user_wise_item_groups.get(d.parent)
|
||||
or r.item_group == d.item_group
|
||||
or r.item_group in item_group_parent_child_map.get(d.item_group, [])
|
||||
)
|
||||
):
|
||||
details[p_key] += r.get(qty_or_amount_field, 0)
|
||||
details[variance_key] = details.get(p_key) - details.get(target_key)
|
||||
@@ -204,6 +209,25 @@ def prepare_data(
|
||||
return rows
|
||||
|
||||
|
||||
def get_item_group_parent_child_map():
|
||||
"""
|
||||
Returns a dict of all item group parents and leaf children associated with them.
|
||||
"""
|
||||
|
||||
item_groups = frappe.get_all(
|
||||
"Item Group", fields=["name", "parent_item_group"], order_by="lft desc, rgt desc"
|
||||
)
|
||||
item_group_parent_child_map = {}
|
||||
|
||||
for item_group in item_groups:
|
||||
children = item_group_parent_child_map.get(item_group.name, [])
|
||||
if not children:
|
||||
children = [item_group.name]
|
||||
item_group_parent_child_map.setdefault(item_group.parent_item_group, []).extend(children)
|
||||
|
||||
return item_group_parent_child_map
|
||||
|
||||
|
||||
def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_field):
|
||||
fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1)
|
||||
|
||||
|
||||
@@ -205,8 +205,11 @@ def clear_demo_record(document):
|
||||
if key not in valid_columns:
|
||||
filters.pop(key, None)
|
||||
|
||||
doc = frappe.get_doc(document_type, filters)
|
||||
doc.delete(ignore_permissions=True)
|
||||
try:
|
||||
doc = frappe.get_doc(document_type, filters)
|
||||
doc.delete(ignore_permissions=True)
|
||||
except frappe.exceptions.DoesNotExistError:
|
||||
pass
|
||||
|
||||
|
||||
def delete_company(company):
|
||||
|
||||
@@ -12,10 +12,11 @@ frappe.ui.form.on("Company", {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
frm.call("check_if_transactions_exist").then((r) => {
|
||||
frm.toggle_enable("default_currency", !r.message);
|
||||
});
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.call("check_if_transactions_exist").then((r) => {
|
||||
frm.toggle_enable("default_currency", !r.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
setup: function (frm) {
|
||||
frm.__rename_queue = "long";
|
||||
@@ -251,7 +252,10 @@ erpnext.company.setup_queries = function (frm) {
|
||||
["discount_allowed_account", { root_type: "Expense" }],
|
||||
["discount_received_account", { root_type: "Income" }],
|
||||
["exchange_gain_loss_account", { root_type: ["in", ["Expense", "Income"]] }],
|
||||
["unrealized_exchange_gain_loss_account", { root_type: ["in", ["Expense", "Income"]] }],
|
||||
[
|
||||
"unrealized_exchange_gain_loss_account",
|
||||
{ root_type: ["in", ["Expense", "Income", "Equity", "Liability"]] },
|
||||
],
|
||||
[
|
||||
"accumulated_depreciation_account",
|
||||
{ root_type: "Asset", account_type: "Accumulated Depreciation" },
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"default_finance_book",
|
||||
"advance_payments_section",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"column_break_fwcf",
|
||||
"default_advance_received_account",
|
||||
"default_advance_paid_account",
|
||||
@@ -779,6 +780,14 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.book_advance_payments_in_separate_party_account",
|
||||
"description": "If <b>Enabled</b> - Reconciliation happens on the <b>Advance Payment posting date</b><br>\nIf <b>Disabled</b> - Reconciliation happens on oldest of 2 Dates: <b>Invoice Date</b> or the <b>Advance Payment posting date</b><br>\n",
|
||||
"fieldname": "reconcile_on_advance_payment_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Reconcile on Advance Payment Date"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-building",
|
||||
@@ -786,7 +795,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-23 12:38:33.173938",
|
||||
"modified": "2024-05-27 17:32:49.057386",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
@@ -842,6 +851,10 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Auditor",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -85,6 +85,7 @@ class Company(NestedSet):
|
||||
parent_company: DF.Link | None
|
||||
payment_terms: DF.Link | None
|
||||
phone_no: DF.Data | None
|
||||
reconcile_on_advance_payment_date: DF.Check
|
||||
registration_details: DF.Code | None
|
||||
rgt: DF.Int
|
||||
round_off_account: DF.Link | None
|
||||
|
||||
@@ -8,8 +8,11 @@ from pypika import Order
|
||||
class DeprecatedSerialNoValuation:
|
||||
@deprecated
|
||||
def calculate_stock_value_from_deprecarated_ledgers(self):
|
||||
if not frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
|
||||
if not frappe.db.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["name"],
|
||||
filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": self.sle.item_code},
|
||||
limit=1,
|
||||
):
|
||||
return
|
||||
|
||||
@@ -41,6 +44,12 @@ class DeprecatedSerialNoValuation:
|
||||
# get rate from serial nos within same company
|
||||
incoming_values = 0.0
|
||||
for serial_no in serial_nos:
|
||||
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
|
||||
if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company:
|
||||
self.serial_no_incoming_rate[serial_no] += flt(sn_details.purchase_rate)
|
||||
incoming_values += self.serial_no_incoming_rate[serial_no]
|
||||
continue
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
stock_ledgers = (
|
||||
frappe.qb.from_(table)
|
||||
|
||||
@@ -47,9 +47,14 @@ frappe.ui.form.on("Batch", {
|
||||
},
|
||||
make_dashboard: (frm) => {
|
||||
if (!frm.is_new()) {
|
||||
let for_stock_levels = 0;
|
||||
if (!frm.doc.batch_qty && frm.doc.expiry_date) {
|
||||
for_stock_levels = 1;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
|
||||
args: { batch_no: frm.doc.name, item_code: frm.doc.item },
|
||||
args: { batch_no: frm.doc.name, item_code: frm.doc.item, for_stock_levels: for_stock_levels },
|
||||
callback: (r) => {
|
||||
if (!r.message) {
|
||||
return;
|
||||
|
||||
@@ -199,6 +199,7 @@ def get_batch_qty(
|
||||
posting_date=None,
|
||||
posting_time=None,
|
||||
ignore_voucher_nos=None,
|
||||
for_stock_levels=False,
|
||||
):
|
||||
"""Returns batch actual qty if warehouse is passed,
|
||||
or returns dict of qty by warehouse if warehouse is None
|
||||
@@ -207,7 +208,8 @@ def get_batch_qty(
|
||||
|
||||
:param batch_no: Optional - give qty for this batch no
|
||||
:param warehouse: Optional - give qty for this warehouse
|
||||
:param item_code: Optional - give qty for this item"""
|
||||
:param item_code: Optional - give qty for this item
|
||||
:param for_stock_levels: True consider expired batches"""
|
||||
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
@@ -222,6 +224,7 @@ def get_batch_qty(
|
||||
"posting_time": posting_time,
|
||||
"batch_no": batch_no,
|
||||
"ignore_voucher_nos": ignore_voucher_nos,
|
||||
"for_stock_levels": for_stock_levels,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ frappe.listview_settings["Batch"] = {
|
||||
get_indicator: (doc) => {
|
||||
if (doc.disabled) {
|
||||
return [__("Disabled"), "gray", "disabled,=,1"];
|
||||
} else if (!doc.batch_qty) {
|
||||
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
|
||||
} else if (
|
||||
doc.expiry_date &&
|
||||
frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0
|
||||
@@ -14,6 +12,8 @@ frappe.listview_settings["Batch"] = {
|
||||
"red",
|
||||
"expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0",
|
||||
];
|
||||
} else if (!doc.batch_qty) {
|
||||
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
|
||||
} else {
|
||||
return [__("Active"), "green", "batch_qty,>,0|disabled,=,0"];
|
||||
}
|
||||
|
||||
@@ -467,6 +467,7 @@ class DeliveryNote(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
@@ -1371,6 +1372,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if source_parent.doctype == "Delivery Note" and source.received_qty:
|
||||
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
|
||||
|
||||
if source.get("use_serial_batch_fields"):
|
||||
target.set("use_serial_batch_fields", 1)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
doctype,
|
||||
source_name,
|
||||
|
||||
@@ -249,18 +249,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertTrue(dn.items[0].serial_no)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
# return entry
|
||||
dn1 = make_sales_return(dn.name)
|
||||
|
||||
dn1.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn1.save()
|
||||
dn1.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[0:2])
|
||||
dn1.submit()
|
||||
dn1.reload()
|
||||
|
||||
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos1:
|
||||
@@ -269,21 +266,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn2 = make_sales_return(dn.name)
|
||||
|
||||
dn2.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn2.save()
|
||||
dn2.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[2:4])
|
||||
dn2.submit()
|
||||
dn2.reload()
|
||||
|
||||
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos2:
|
||||
self.assertTrue(serial_no in serial_nos)
|
||||
self.assertFalse(serial_no in returned_serial_nos1)
|
||||
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
def test_sales_return_for_non_bundled_items_partial(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -428,7 +419,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(dn.per_returned, 100)
|
||||
self.assertEqual(dn.status, "Return Issued")
|
||||
|
||||
def test_delivery_note_return_valuation_on_different_warehuose(self):
|
||||
def test_delivery_note_return_valuation_on_different_warehouse(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -5,7 +5,7 @@ import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
|
||||
@@ -469,6 +469,13 @@ class Item(Document):
|
||||
def validate_warehouse_for_reorder(self):
|
||||
"""Validate Reorder level table for duplicate and conditional mandatory"""
|
||||
warehouse_material_request_type: list[tuple[str, str]] = []
|
||||
|
||||
_warehouse_before_save = frappe._dict()
|
||||
if not self.is_new() and self._doc_before_save:
|
||||
_warehouse_before_save = {
|
||||
d.name: d.warehouse for d in self._doc_before_save.get("reorder_levels") or []
|
||||
}
|
||||
|
||||
for d in self.get("reorder_levels"):
|
||||
if not d.warehouse_group:
|
||||
d.warehouse_group = d.warehouse
|
||||
@@ -485,6 +492,19 @@ class Item(Document):
|
||||
if d.warehouse_reorder_level and not d.warehouse_reorder_qty:
|
||||
frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx))
|
||||
|
||||
if d.warehouse_group and d.warehouse:
|
||||
if _warehouse_before_save.get(d.name) == d.warehouse:
|
||||
continue
|
||||
|
||||
child_warehouses = get_child_warehouses(d.warehouse_group)
|
||||
if d.warehouse not in child_warehouses:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}"
|
||||
).format(d.idx, bold(d.warehouse), bold(d.warehouse_group)),
|
||||
title=_("Incorrect Check in (group) Warehouse for Reorder"),
|
||||
)
|
||||
|
||||
def stock_ledger_created(self):
|
||||
if not hasattr(self, "_stock_ledger_created"):
|
||||
self._stock_ledger_created = len(
|
||||
@@ -1360,3 +1380,10 @@ def get_asset_naming_series():
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
|
||||
|
||||
return get_asset_naming_series()
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_child_warehouses(warehouse):
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
|
||||
return get_child_warehouses(warehouse)
|
||||
|
||||
@@ -862,6 +862,27 @@ class TestItem(FrappeTestCase):
|
||||
self.assertEqual(data[0].description, item.description)
|
||||
self.assertTrue("description" in data[0])
|
||||
|
||||
def test_group_warehouse_for_reorder_item(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
item_doc = make_item("_Test Group Warehouse For Reorder Item", {"is_stock_item": 1})
|
||||
warehouse = create_warehouse("_Test Warehouse - _TC")
|
||||
warehouse_doc = frappe.get_doc("Warehouse", warehouse)
|
||||
warehouse_doc.db_set("parent_warehouse", "")
|
||||
|
||||
item_doc.append(
|
||||
"reorder_levels",
|
||||
{
|
||||
"warehouse": warehouse,
|
||||
"warehouse_reorder_level": 10,
|
||||
"warehouse_reorder_qty": 100,
|
||||
"material_request_type": "Purchase",
|
||||
"warehouse_group": "_Test Warehouse Group - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, item_doc.save)
|
||||
|
||||
|
||||
def set_item_variant_settings(fields):
|
||||
doc = frappe.get_doc("Item Variant Settings")
|
||||
|
||||
@@ -167,6 +167,7 @@ class TestPickList(FrappeTestCase):
|
||||
"item_code": "_Test Serialized Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"valuation_rate": 100,
|
||||
"reconcile_all_serial_batch": 1,
|
||||
"qty": 5,
|
||||
"serial_and_batch_bundle": make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
|
||||
@@ -370,6 +370,7 @@ class PurchaseReceipt(BuyingController):
|
||||
else:
|
||||
self.db_set("status", "Completed")
|
||||
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty, reserved_qty_for_subcontract in bin
|
||||
@@ -923,6 +924,15 @@ class PurchaseReceipt(BuyingController):
|
||||
notify=True,
|
||||
)
|
||||
|
||||
def enable_recalculate_rate_in_sles(self):
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
(
|
||||
frappe.qb.update(sle_table)
|
||||
.set(sle_table.recalculate_rate, 1)
|
||||
.where(sle_table.voucher_no == self.name)
|
||||
.where(sle_table.voucher_type == "Purchase Receipt")
|
||||
).run()
|
||||
|
||||
|
||||
def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
|
||||
return frappe.db.get_value(
|
||||
@@ -1095,15 +1105,10 @@ def adjust_incoming_rate_for_pr(doc):
|
||||
for item in doc.get("items"):
|
||||
item.db_update()
|
||||
|
||||
doc.docstatus = 2
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries_on_cancel()
|
||||
if doc.doctype == "Purchase Receipt":
|
||||
doc.enable_recalculate_rate_in_sles()
|
||||
|
||||
# update stock & gl entries for submit state of PR
|
||||
doc.docstatus = 1
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries()
|
||||
doc.repost_future_sle_and_gle()
|
||||
doc.repost_future_sle_and_gle(force=True)
|
||||
|
||||
|
||||
def get_item_wise_returned_qty(pr_doc):
|
||||
|
||||
@@ -2645,7 +2645,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
for row in inter_transfer_dn_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
|
||||
def test_internal_transfer_with_serial_batch_items_without_use_serial_batch_fields(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"purchase_invoice",
|
||||
"column_break_40",
|
||||
"allow_zero_valuation_rate",
|
||||
"return_qty_from_rejected_warehouse",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
"asset_category",
|
||||
@@ -1116,12 +1117,19 @@
|
||||
"hidden": 1,
|
||||
"label": "Apply TDS",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "return_qty_from_rejected_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Return Qty from Rejected Warehouse",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-08 20:00:16.278292",
|
||||
"modified": "2024-05-28 09:48:24.448815",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
@@ -1132,4 +1140,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class PurchaseReceiptItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
retain_sample: DF.Check
|
||||
return_qty_from_rejected_warehouse: DF.Check
|
||||
returned_qty: DF.Float
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_order: DF.Link | None
|
||||
|
||||
@@ -377,7 +377,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
apply_putaway_rule=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
stock_entry.load_from_db()
|
||||
|
||||
self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
|
||||
@@ -398,11 +398,17 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
for row in stock_entry.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
|
||||
stock_entry.load_from_db()
|
||||
stock_entry.cancel()
|
||||
|
||||
rivs = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name})
|
||||
for row in rivs:
|
||||
riv_doc = frappe.get_doc("Repost Item Valuation", row.name)
|
||||
riv_doc.cancel()
|
||||
riv_doc.delete()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)
|
||||
|
||||
stock_entry.delete()
|
||||
pr.cancel()
|
||||
rule_1.delete()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user