mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 10:49:09 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into immutable_ledger
This commit is contained in:
@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not amount:
|
if not amount:
|
||||||
return
|
|
||||||
|
|
||||||
gl_posting_date = end_date
|
|
||||||
prev_posting_date = None
|
|
||||||
# check if books nor frozen till endate:
|
|
||||||
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
|
||||||
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
|
||||||
prev_posting_date = end_date
|
prev_posting_date = end_date
|
||||||
|
|
||||||
if via_journal_entry:
|
|
||||||
book_revenue_via_journal_entry(
|
|
||||||
doc,
|
|
||||||
credit_account,
|
|
||||||
debit_account,
|
|
||||||
amount,
|
|
||||||
base_amount,
|
|
||||||
gl_posting_date,
|
|
||||||
project,
|
|
||||||
account_currency,
|
|
||||||
item.cost_center,
|
|
||||||
item,
|
|
||||||
deferred_process,
|
|
||||||
submit_journal_entry,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
make_gl_entries(
|
gl_posting_date = end_date
|
||||||
doc,
|
prev_posting_date = None
|
||||||
credit_account,
|
# check if books nor frozen till endate:
|
||||||
debit_account,
|
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
|
||||||
against,
|
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||||
amount,
|
prev_posting_date = end_date
|
||||||
base_amount,
|
|
||||||
gl_posting_date,
|
if via_journal_entry:
|
||||||
project,
|
book_revenue_via_journal_entry(
|
||||||
account_currency,
|
doc,
|
||||||
item.cost_center,
|
credit_account,
|
||||||
item,
|
debit_account,
|
||||||
deferred_process,
|
amount,
|
||||||
)
|
base_amount,
|
||||||
|
gl_posting_date,
|
||||||
|
project,
|
||||||
|
account_currency,
|
||||||
|
item.cost_center,
|
||||||
|
item,
|
||||||
|
deferred_process,
|
||||||
|
submit_journal_entry,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
make_gl_entries(
|
||||||
|
doc,
|
||||||
|
credit_account,
|
||||||
|
debit_account,
|
||||||
|
against,
|
||||||
|
amount,
|
||||||
|
base_amount,
|
||||||
|
gl_posting_date,
|
||||||
|
project,
|
||||||
|
account_currency,
|
||||||
|
item.cost_center,
|
||||||
|
item,
|
||||||
|
deferred_process,
|
||||||
|
)
|
||||||
|
|
||||||
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
|
||||||
if frappe.flags.deferred_accounting_error:
|
if frappe.flags.deferred_accounting_error:
|
||||||
|
|||||||
@@ -65,6 +65,8 @@
|
|||||||
"label": "Is Group"
|
"label": "Is Group"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "parent_account.company",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
|
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
|
||||||
"fieldname": "check_supplier_invoice_uniqueness",
|
"fieldname": "check_supplier_invoice_uniqueness",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Check Supplier Invoice Number Uniqueness"
|
"label": "Check Supplier Invoice Number Uniqueness"
|
||||||
@@ -498,4 +498,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company"
|
"options": "Company"
|
||||||
},
|
},
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:37.922473",
|
"modified": "2024-04-28 14:40:50.910884",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Reconciliation Tool",
|
"name": "Bank Reconciliation Tool",
|
||||||
|
|||||||
@@ -719,7 +719,7 @@ def get_pe_matching_query(
|
|||||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||||
ConstantColumn("Payment Entry").as_("doctype"),
|
ConstantColumn("Payment Entry").as_("doctype"),
|
||||||
pe.name,
|
pe.name,
|
||||||
pe.paid_amount,
|
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||||
pe.reference_no,
|
pe.reference_no,
|
||||||
pe.reference_date,
|
pe.reference_date,
|
||||||
pe.party,
|
pe.party,
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
if self.party_type not in ("Customer", "Supplier"):
|
if self.party_type not in ("Customer", "Supplier"):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1225,88 +1226,71 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||||
if self.book_advance_payments_in_separate_party_account:
|
|
||||||
|
for d in self.get("references"):
|
||||||
|
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||||
|
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||||
|
cost_center = self.cost_center
|
||||||
|
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||||
|
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
gle = party_gl_dict.copy()
|
||||||
|
|
||||||
if self.payment_type == "Receive":
|
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||||
amount = self.base_paid_amount
|
reverse_dr_or_cr = 0
|
||||||
else:
|
|
||||||
amount = self.base_received_amount
|
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
|
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||||
|
payable_party_types = get_party_types_from_account_type("Payable")
|
||||||
|
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||||
|
if (
|
||||||
|
is_return
|
||||||
|
and self.party_type in receivable_party_types
|
||||||
|
and (self.payment_type == "Pay")
|
||||||
|
):
|
||||||
|
reverse_dr_or_cr = 1
|
||||||
|
elif (
|
||||||
|
is_return
|
||||||
|
and self.party_type in payable_party_types
|
||||||
|
and (self.payment_type == "Receive")
|
||||||
|
):
|
||||||
|
reverse_dr_or_cr = 1
|
||||||
|
|
||||||
|
if is_return and not reverse_dr_or_cr:
|
||||||
|
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
exchange_rate = self.get_exchange_rate()
|
|
||||||
amount_in_account_currency = amount * exchange_rate
|
|
||||||
gle.update(
|
gle.update(
|
||||||
{
|
{
|
||||||
dr_or_cr: amount,
|
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||||
dr_or_cr + "_in_account_currency": amount_in_account_currency,
|
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||||
"against_voucher_type": "Payment Entry",
|
"against_voucher_type": d.reference_doctype,
|
||||||
"against_voucher": self.name,
|
"against_voucher": d.reference_name,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": cost_center,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
gl_entries.append(gle)
|
gl_entries.append(gle)
|
||||||
else:
|
|
||||||
for d in self.get("references"):
|
|
||||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
|
||||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
|
||||||
cost_center = self.cost_center
|
|
||||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
|
||||||
cost_center = frappe.db.get_value(
|
|
||||||
d.reference_doctype, d.reference_name, "cost_center"
|
|
||||||
)
|
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
if self.unallocated_amount:
|
||||||
|
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||||
|
exchange_rate = self.get_exchange_rate()
|
||||||
|
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||||
|
|
||||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
|
gle = party_gl_dict.copy()
|
||||||
d
|
gle.update(
|
||||||
)
|
{
|
||||||
reverse_dr_or_cr = 0
|
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||||
|
dr_or_cr: base_unallocated_amount,
|
||||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
}
|
||||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
)
|
||||||
payable_party_types = get_party_types_from_account_type("Payable")
|
|
||||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
|
||||||
if (
|
|
||||||
is_return
|
|
||||||
and self.party_type in receivable_party_types
|
|
||||||
and (self.payment_type == "Pay")
|
|
||||||
):
|
|
||||||
reverse_dr_or_cr = 1
|
|
||||||
elif (
|
|
||||||
is_return
|
|
||||||
and self.party_type in payable_party_types
|
|
||||||
and (self.payment_type == "Receive")
|
|
||||||
):
|
|
||||||
reverse_dr_or_cr = 1
|
|
||||||
|
|
||||||
if is_return and not reverse_dr_or_cr:
|
|
||||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
|
||||||
|
|
||||||
|
if self.book_advance_payments_in_separate_party_account:
|
||||||
gle.update(
|
gle.update(
|
||||||
{
|
{
|
||||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
"against_voucher_type": "Payment Entry",
|
||||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
"against_voucher": self.name,
|
||||||
"against_voucher_type": d.reference_doctype,
|
|
||||||
"against_voucher": d.reference_name,
|
|
||||||
"cost_center": cost_center,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
gl_entries.append(gle)
|
gl_entries.append(gle)
|
||||||
|
|
||||||
if self.unallocated_amount:
|
|
||||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
|
||||||
exchange_rate = self.get_exchange_rate()
|
|
||||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
|
||||||
gle.update(
|
|
||||||
{
|
|
||||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
|
||||||
dr_or_cr: base_unallocated_amount,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
gl_entries.append(gle)
|
|
||||||
|
|
||||||
def make_advance_gl_entries(
|
def make_advance_gl_entries(
|
||||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||||
@@ -1321,7 +1305,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
|
||||||
"""
|
"""
|
||||||
If 'entry' is passed, GL enties only for that reference is added.
|
If 'entry' is passed, GL entries only for that reference is added.
|
||||||
"""
|
"""
|
||||||
if self.book_advance_payments_in_separate_party_account:
|
if self.book_advance_payments_in_separate_party_account:
|
||||||
references = [x for x in self.get("references")]
|
references = [x for x in self.get("references")]
|
||||||
@@ -1333,8 +1317,6 @@ class PaymentEntry(AccountsController):
|
|||||||
"Sales Invoice",
|
"Sales Invoice",
|
||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
"Journal Entry",
|
"Journal Entry",
|
||||||
"Sales Order",
|
|
||||||
"Purchase Order",
|
|
||||||
"Payment Entry",
|
"Payment Entry",
|
||||||
):
|
):
|
||||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||||
|
|||||||
@@ -1477,6 +1477,68 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.check_gl_entries()
|
self.check_gl_entries()
|
||||||
self.check_pl_entries()
|
self.check_pl_entries()
|
||||||
|
|
||||||
|
def test_advance_as_liability_against_order(self):
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||||
|
make_purchase_invoice as _make_purchase_invoice,
|
||||||
|
)
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
|
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
advance_account = create_account(
|
||||||
|
parent_account="Current Liabilities - _TC",
|
||||||
|
account_name="Advances Paid",
|
||||||
|
company=company,
|
||||||
|
account_type="Liability",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
company,
|
||||||
|
{
|
||||||
|
"book_advance_payments_in_separate_party_account": 1,
|
||||||
|
"default_advance_paid_account": advance_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
po = create_purchase_order(supplier="_Test Supplier")
|
||||||
|
pe = get_payment_entry("Purchase Order", po.name, bank_account="Cash - _TC")
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
pre_reconciliation_gle = [
|
||||||
|
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||||
|
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.voucher_no = pe.name
|
||||||
|
self.expected_gle = pre_reconciliation_gle
|
||||||
|
self.check_gl_entries()
|
||||||
|
|
||||||
|
# Make Purchase Invoice against the order
|
||||||
|
pi = _make_purchase_invoice(po.name)
|
||||||
|
pi.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": pe.doctype,
|
||||||
|
"reference_name": pe.name,
|
||||||
|
"reference_row": pe.references[0].name,
|
||||||
|
"advance_amount": 5000,
|
||||||
|
"allocated_amount": 5000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi.save().submit()
|
||||||
|
|
||||||
|
# # assert General and Payment Ledger entries post partial reconciliation
|
||||||
|
self.expected_gle = [
|
||||||
|
{"account": pi.credit_to, "debit": 5000.0, "credit": 0.0},
|
||||||
|
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
|
||||||
|
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
|
||||||
|
{"account": advance_account, "debit": 0.0, "credit": 5000.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
self.voucher_no = pe.name
|
||||||
|
self.check_gl_entries()
|
||||||
|
|
||||||
def check_pl_entries(self):
|
def check_pl_entries(self):
|
||||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||||
pl_entries = (
|
pl_entries = (
|
||||||
|
|||||||
@@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
this.frm.set_value("receivable_payable_account", r.message[0]);
|
if (typeof r.message === "string") {
|
||||||
this.frm.set_value("default_advance_account", r.message[1]);
|
this.frm.set_value("receivable_payable_account", r.message);
|
||||||
|
} else if (Array.isArray(r.message)) {
|
||||||
|
this.frm.set_value("receivable_payable_account", r.message[0]);
|
||||||
|
this.frm.set_value("default_advance_account", r.message[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.frm.refresh();
|
this.frm.refresh();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -573,6 +573,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
|||||||
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
|
||||||
# Apply discount on discounted rate
|
# Apply discount on discounted rate
|
||||||
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
|
||||||
|
elif args.price_list_rate:
|
||||||
|
value = pricing_rule.get(field, 0)
|
||||||
|
calculate_discount_percentage = False
|
||||||
|
if field == "discount_percentage":
|
||||||
|
field = "discount_amount"
|
||||||
|
value = args.price_list_rate * (value / 100)
|
||||||
|
calculate_discount_percentage = True
|
||||||
|
|
||||||
|
if field not in item_details:
|
||||||
|
item_details.setdefault(field, 0)
|
||||||
|
|
||||||
|
item_details[field] += value if pricing_rule else args.get(field, 0)
|
||||||
|
if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount:
|
||||||
|
item_details.discount_percentage = flt(
|
||||||
|
(flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if field not in item_details:
|
if field not in item_details:
|
||||||
item_details.setdefault(field, 0)
|
item_details.setdefault(field, 0)
|
||||||
|
|||||||
@@ -1102,7 +1102,60 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
so.load_from_db()
|
so.load_from_db()
|
||||||
self.assertEqual(so.items[1].is_free_item, 1)
|
self.assertEqual(so.items[1].is_free_item, 1)
|
||||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||||
self.assertEqual(so.items[1].qty, 4)
|
self.assertEqual(so.items[1].qty, 3)
|
||||||
|
|
||||||
|
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(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,
|
||||||
|
"apply_multiple_pricing_rules": 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 Amount",
|
||||||
|
"discount_amount": 100,
|
||||||
|
"apply_multiple_pricing_rules": 1,
|
||||||
|
"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_amount, 200)
|
||||||
|
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"]
|
test_dependencies = ["Campaign"]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
@@ -653,7 +654,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
|||||||
if transaction_qty:
|
if transaction_qty:
|
||||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||||
if pricing_rule.round_free_qty:
|
if pricing_rule.round_free_qty:
|
||||||
qty = round(qty)
|
qty = math.floor(qty)
|
||||||
|
|
||||||
free_item_data_args = {
|
free_item_data_args = {
|
||||||
"item_code": free_item,
|
"item_code": free_item,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
|
|||||||
ageing_filters = frappe._dict(
|
ageing_filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"company": doc.company,
|
"company": doc.company,
|
||||||
"report_date": doc.to_date,
|
"report_date": doc.posting_date,
|
||||||
"ageing_based_on": doc.ageing_based_on,
|
"ageing_based_on": doc.ageing_based_on,
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
"range2": 60,
|
"range2": 60,
|
||||||
|
|||||||
@@ -340,10 +340,11 @@
|
|||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 25%">30 Days</th>
|
<th style="width: 25%">0 - 30 Days</th>
|
||||||
<th style="width: 25%">60 Days</th>
|
<th style="width: 25%">30 - 60 Days</th>
|
||||||
<th style="width: 25%">90 Days</th>
|
<th style="width: 25%">60 - 90 Days</th>
|
||||||
<th style="width: 25%">120 Days</th>
|
<th style="width: 25%">90 - 120 Days</th>
|
||||||
|
<th style="width: 20%">Above 120 Days</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -352,6 +353,7 @@
|
|||||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||||
|
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -11,13 +11,15 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "cost_center_name",
|
"fieldname": "cost_center_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:23.244686",
|
"modified": "2024-05-03 17:16:51.666461",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "PSOA Cost Center",
|
"name": "PSOA Cost Center",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
cost_center_name: DF.Link | None
|
cost_center_name: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -1063,7 +1063,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check if the exchange rate has changed
|
# check if the exchange rate has changed
|
||||||
if item.get("purchase_receipt"):
|
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||||
if (
|
if (
|
||||||
exchange_rate_map[item.purchase_receipt]
|
exchange_rate_map[item.purchase_receipt]
|
||||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||||
|
|||||||
@@ -2042,7 +2042,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contact_and_address_tab",
|
"fieldname": "contact_and_address_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact & Address"
|
"label": "Address & Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "payments_tab",
|
"fieldname": "payments_tab",
|
||||||
@@ -2203,7 +2203,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-04-11 11:30:26.272441",
|
"modified": "2024-05-08 18:02:28.549041",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -393,6 +393,9 @@ class SalesInvoice(SellingController):
|
|||||||
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
def set_tax_withholding(self):
|
||||||
|
if self.get("is_opening") == "Yes":
|
||||||
|
return
|
||||||
|
|
||||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||||
|
|
||||||
if not tax_withholding_details:
|
if not tax_withholding_details:
|
||||||
|
|||||||
@@ -1783,6 +1783,49 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertTrue(gle)
|
self.assertTrue(gle)
|
||||||
|
|
||||||
|
def test_gle_in_transaction_currency(self):
|
||||||
|
# create multi currency sales invoice with 2 items with same income account
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer USD",
|
||||||
|
debit_to="_Test Receivable USD - _TC",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
# add 2nd item with same income account
|
||||||
|
si.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 80,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
gl_entries = frappe.db.sql(
|
||||||
|
"""select transaction_currency, transaction_exchange_rate,
|
||||||
|
debit_in_transaction_currency, credit_in_transaction_currency
|
||||||
|
from `tabGL Entry`
|
||||||
|
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
|
||||||
|
order by account asc""",
|
||||||
|
si.name,
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_gle = {
|
||||||
|
"transaction_currency": "USD",
|
||||||
|
"transaction_exchange_rate": 50,
|
||||||
|
"debit_in_transaction_currency": 0,
|
||||||
|
"credit_in_transaction_currency": 180,
|
||||||
|
}
|
||||||
|
|
||||||
|
for gle in gl_entries:
|
||||||
|
for field in expected_gle:
|
||||||
|
self.assertEqual(expected_gle[field], gle[field])
|
||||||
|
|
||||||
def test_invoice_exchange_rate(self):
|
def test_invoice_exchange_rate(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
customer="_Test Customer USD",
|
customer="_Test Customer USD",
|
||||||
|
|||||||
@@ -112,11 +112,7 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
_current_invoice_start = None
|
_current_invoice_start = None
|
||||||
|
|
||||||
if (
|
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||||
self.is_new_subscription()
|
|
||||||
and self.trial_period_end
|
|
||||||
and getdate(self.trial_period_end) > getdate(self.start_date)
|
|
||||||
):
|
|
||||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||||
elif self.trial_period_start and self.is_trialling():
|
elif self.trial_period_start and self.is_trialling():
|
||||||
_current_invoice_start = self.trial_period_start
|
_current_invoice_start = self.trial_period_start
|
||||||
@@ -143,7 +139,7 @@ class Subscription(Document):
|
|||||||
else:
|
else:
|
||||||
billing_cycle_info = self.get_billing_cycle_data()
|
billing_cycle_info = self.get_billing_cycle_data()
|
||||||
if billing_cycle_info:
|
if billing_cycle_info:
|
||||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
|
if getdate(self.start_date) < getdate(date):
|
||||||
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||||
|
|
||||||
# For cases where trial period is for an entire billing interval
|
# For cases where trial period is for an entire billing interval
|
||||||
@@ -234,14 +230,14 @@ class Subscription(Document):
|
|||||||
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||||
self.status = "Past Due Date"
|
self.status = "Past Due Date"
|
||||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
elif not self.has_outstanding_invoice():
|
||||||
self.status = "Active"
|
self.status = "Active"
|
||||||
|
|
||||||
def is_trialling(self) -> bool:
|
def is_trialling(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns `True` if the `Subscription` is in trial period.
|
Returns `True` if the `Subscription` is in trial period.
|
||||||
"""
|
"""
|
||||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
return not self.period_has_passed(self.trial_period_end)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def period_has_passed(
|
def period_has_passed(
|
||||||
@@ -288,14 +284,6 @@ class Subscription(Document):
|
|||||||
def invoice_document_type(self) -> str:
|
def invoice_document_type(self) -> str:
|
||||||
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||||
|
|
||||||
def is_new_subscription(self) -> bool:
|
|
||||||
"""
|
|
||||||
Returns `True` if `Subscription` has never generated an invoice
|
|
||||||
"""
|
|
||||||
return self.is_new() or not frappe.db.exists(
|
|
||||||
{"doctype": self.invoice_document_type, "subscription": self.name}
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
def validate(self) -> None:
|
||||||
self.validate_trial_period()
|
self.validate_trial_period()
|
||||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||||
@@ -604,7 +592,7 @@ class Subscription(Document):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
||||||
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
|
getdate(posting_date) == getdate(self.current_invoice_start)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
||||||
|
|||||||
@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
|
|||||||
|
|
||||||
# Process subscription and create first invoice
|
# Process subscription and create first invoice
|
||||||
# Subscription status will be unpaid since due date has already passed
|
# Subscription status will be unpaid since due date has already passed
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-04-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
def test_multi_currency_subscription(self):
|
def test_multi_currency_subscription(self):
|
||||||
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
|
|||||||
party=party,
|
party=party,
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:52.419915",
|
"modified": "2024-04-30 10:26:48.218294",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withholding Account",
|
"name": "Tax Withholding Account",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
|
|||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
from frappe.utils import cint, flt, getdate
|
from frappe.utils import cint, flt, getdate
|
||||||
|
|
||||||
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
|
|
||||||
|
|
||||||
class TaxWithholdingCategory(Document):
|
class TaxWithholdingCategory(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
|
|||||||
if d.get("account") in existing_accounts:
|
if d.get("account") in existing_accounts:
|
||||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||||
|
|
||||||
|
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||||
existing_accounts.append(d.get("account"))
|
existing_accounts.append(d.get("account"))
|
||||||
|
|
||||||
def validate_thresholds(self):
|
def validate_thresholds(self):
|
||||||
@@ -282,6 +285,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
if taxable_vouchers:
|
if taxable_vouchers:
|
||||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||||
|
|
||||||
|
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
|
||||||
|
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
|
||||||
|
# rest of the below logic to function properly
|
||||||
|
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
|
||||||
|
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
|
||||||
|
if tax_deducted_on_advances:
|
||||||
|
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
|
||||||
|
|
||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
|
|
||||||
if party_type == "Supplier":
|
if party_type == "Supplier":
|
||||||
@@ -418,7 +429,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
|||||||
frappe.qb.from_(at)
|
frappe.qb.from_(at)
|
||||||
.inner_join(pe)
|
.inner_join(pe)
|
||||||
.on(pe.name == at.parent)
|
.on(pe.name == at.parent)
|
||||||
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
|
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
|
||||||
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
|
||||||
.where(at.parent.isin(advances))
|
.where(at.parent.isin(advances))
|
||||||
.where(at.account_head == tax_details.account_head)
|
.where(at.account_head == tax_details.account_head)
|
||||||
@@ -443,6 +454,16 @@ def get_deducted_tax(taxable_vouchers, tax_details):
|
|||||||
return sum(entries)
|
return sum(entries)
|
||||||
|
|
||||||
|
|
||||||
|
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
|
||||||
|
"""
|
||||||
|
Only applies for Taxes deducted on Advance Payments
|
||||||
|
"""
|
||||||
|
advance_tax_from_across_fiscal_year = sum(
|
||||||
|
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
|
||||||
|
)
|
||||||
|
return advance_tax_from_across_fiscal_year
|
||||||
|
|
||||||
|
|
||||||
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
|
import datetime
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
from frappe.utils import today
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||||
|
|
||||||
test_dependencies = ["Supplier Group", "Customer Group"]
|
test_dependencies = ["Supplier Group", "Customer Group"]
|
||||||
|
|
||||||
|
|
||||||
class TestTaxWithholdingCategory(unittest.TestCase):
|
class TestTaxWithholdingCategory(FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
# create relevant supplier, etc
|
# create relevant supplier, etc
|
||||||
@@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
make_pan_no_field()
|
make_pan_no_field()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
cancel_invoices()
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_cumulative_threshold_tds(self):
|
def test_cumulative_threshold_tds(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
|
||||||
|
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||||
)
|
)
|
||||||
@@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
pi2.cancel()
|
pi2.cancel()
|
||||||
pi3.cancel()
|
pi3.cancel()
|
||||||
|
|
||||||
|
def set_previous_fy_and_tax_category(self):
|
||||||
|
test_company = "_Test Company"
|
||||||
|
category = "Cumulative Threshold TDS"
|
||||||
|
|
||||||
|
def add_company_to_fy(fy, company):
|
||||||
|
if not [x.company for x in fy.companies if x.company == company]:
|
||||||
|
fy.append("companies", {"company": company})
|
||||||
|
fy.save()
|
||||||
|
|
||||||
|
# setup previous fiscal year
|
||||||
|
fiscal_year = get_fiscal_year(today(), company=test_company)
|
||||||
|
if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)):
|
||||||
|
self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0])
|
||||||
|
add_company_to_fy(self.prev_fy, test_company)
|
||||||
|
else:
|
||||||
|
# make previous fiscal year
|
||||||
|
start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day)
|
||||||
|
end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day)
|
||||||
|
self.prev_fy = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Fiscal Year",
|
||||||
|
"year_start_date": start,
|
||||||
|
"year_end_date": end,
|
||||||
|
"companies": [{"company": test_company}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.prev_fy.save()
|
||||||
|
|
||||||
|
# setup tax withholding category for previous fiscal year
|
||||||
|
cat = frappe.get_doc("Tax Withholding Category", category)
|
||||||
|
cat.append(
|
||||||
|
"rates",
|
||||||
|
{
|
||||||
|
"from_date": self.prev_fy.year_start_date,
|
||||||
|
"to_date": self.prev_fy.year_end_date,
|
||||||
|
"tax_withholding_rate": 10,
|
||||||
|
"single_threshold": 0,
|
||||||
|
"cumulative_threshold": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cat.save()
|
||||||
|
|
||||||
|
def test_tds_across_fiscal_year(self):
|
||||||
|
"""
|
||||||
|
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
|
||||||
|
--||-----FY 2023-----||-----FY 2024-----||--
|
||||||
|
--||-----Advance-----||---Inv1---Inv2---||--
|
||||||
|
"""
|
||||||
|
self.set_previous_fy_and_tax_category()
|
||||||
|
supplier = "Test TDS Supplier"
|
||||||
|
# Cumulative threshold 30000 and tax rate 10%
|
||||||
|
category = "Cumulative Threshold TDS"
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Supplier",
|
||||||
|
supplier,
|
||||||
|
{
|
||||||
|
"tax_withholding_category": category,
|
||||||
|
"pan": "ABCTY1234D",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10)
|
||||||
|
po = create_purchase_order(supplier=supplier, qty=10, rate=10000)
|
||||||
|
po.transaction_date = po_and_advance_posting_date
|
||||||
|
po.taxes = []
|
||||||
|
po.apply_tds = False
|
||||||
|
po.tax_withholding_category = None
|
||||||
|
po.save().submit()
|
||||||
|
|
||||||
|
# Partial advance
|
||||||
|
payment = get_payment_entry(po.doctype, po.name)
|
||||||
|
payment.posting_date = po_and_advance_posting_date
|
||||||
|
payment.paid_amount = 60000
|
||||||
|
payment.apply_tax_withholding_amount = 1
|
||||||
|
payment.tax_withholding_category = category
|
||||||
|
payment.references = []
|
||||||
|
payment.taxes = []
|
||||||
|
payment.save().submit()
|
||||||
|
|
||||||
|
self.assertEqual(len(payment.taxes), 1)
|
||||||
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
|
|
||||||
|
# Multiple partial invoices
|
||||||
|
payment.reload()
|
||||||
|
pi1 = make_purchase_invoice(source_name=po.name)
|
||||||
|
pi1.apply_tds = True
|
||||||
|
pi1.tax_withholding_category = category
|
||||||
|
pi1.items[0].qty = 3
|
||||||
|
pi1.items[0].rate = 10000
|
||||||
|
advances = pi1.get_advance_entries()
|
||||||
|
pi1.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"advance_amount": advances[0].amount,
|
||||||
|
"allocated_amount": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi1.save().submit()
|
||||||
|
pi1.reload()
|
||||||
|
payment.reload()
|
||||||
|
self.assertEqual(pi1.taxes, [])
|
||||||
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
|
self.assertEqual(payment.taxes[0].allocated_amount, 3000)
|
||||||
|
|
||||||
|
pi2 = make_purchase_invoice(source_name=po.name)
|
||||||
|
pi2.apply_tds = True
|
||||||
|
pi2.tax_withholding_category = category
|
||||||
|
pi2.items[0].qty = 3
|
||||||
|
pi2.items[0].rate = 10000
|
||||||
|
advances = pi2.get_advance_entries()
|
||||||
|
pi2.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"advance_amount": advances[0].amount,
|
||||||
|
"allocated_amount": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi2.save().submit()
|
||||||
|
pi2.reload()
|
||||||
|
payment.reload()
|
||||||
|
self.assertEqual(pi2.taxes, [])
|
||||||
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
|
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||||
|
|
||||||
|
|
||||||
def cancel_invoices():
|
def cancel_invoices():
|
||||||
purchase_invoices = frappe.get_all(
|
purchase_invoices = frappe.get_all(
|
||||||
|
|||||||
@@ -241,10 +241,16 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||||
entry.debit_in_account_currency
|
entry.debit_in_account_currency
|
||||||
)
|
)
|
||||||
|
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
||||||
|
entry.debit_in_transaction_currency
|
||||||
|
)
|
||||||
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
||||||
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
||||||
entry.credit_in_account_currency
|
entry.credit_in_account_currency
|
||||||
)
|
)
|
||||||
|
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
||||||
|
entry.credit_in_transaction_currency
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
merged_gl_map.append(entry)
|
merged_gl_map.append(entry)
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ def set_address_details(
|
|||||||
*,
|
*,
|
||||||
ignore_permissions=False,
|
ignore_permissions=False,
|
||||||
):
|
):
|
||||||
billing_address_field = "customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
billing_address_field = (
|
||||||
|
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
|
||||||
|
)
|
||||||
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
||||||
if doctype:
|
if doctype:
|
||||||
party_details.update(
|
party_details.update(
|
||||||
|
|||||||
@@ -501,8 +501,9 @@ class ReceivablePayableReport:
|
|||||||
# Deduct that from paid amount pre allocation
|
# Deduct that from paid amount pre allocation
|
||||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||||
|
|
||||||
# If no or single payment terms, no need to split the row
|
# If single payment terms, no need to split the row
|
||||||
if len(payment_terms_details) <= 1:
|
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
|
||||||
|
self.append_payment_term(row, payment_terms_details[0], original_row)
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in payment_terms_details:
|
for d in payment_terms_details:
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
|||||||
filters.end_date = fiscal_year.year_end_date
|
filters.end_date = fiscal_year.year_end_date
|
||||||
|
|
||||||
for company in companies:
|
for company in companies:
|
||||||
|
filters.company = company
|
||||||
amount = get_account_type_based_gl_data(company, filters)
|
amount = get_account_type_based_gl_data(company, filters)
|
||||||
|
|
||||||
if amount and account_type == "Depreciation":
|
if amount and account_type == "Depreciation":
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ class Deferred_Item:
|
|||||||
For a given GL/Journal posting, get balance based on item type
|
For a given GL/Journal posting, get balance based on item type
|
||||||
"""
|
"""
|
||||||
if self.type == "Deferred Sale Item":
|
if self.type == "Deferred Sale Item":
|
||||||
return entry.debit - entry.credit
|
return flt(entry.debit) - flt(entry.credit)
|
||||||
elif self.type == "Deferred Purchase Item":
|
elif self.type == "Deferred Purchase Item":
|
||||||
return -(entry.credit - entry.debit)
|
return -(flt(entry.credit) - flt(entry.debit))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_item_total(self):
|
def get_item_total(self):
|
||||||
@@ -147,7 +147,7 @@ class Deferred_Item:
|
|||||||
actual = 0
|
actual = 0
|
||||||
for posting in self.gle_entries:
|
for posting in self.gle_entries:
|
||||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date:
|
||||||
period_sum += self.get_amount(posting)
|
period_sum += self.get_amount(posting)
|
||||||
if posting.posted == "posted":
|
if posting.posted == "posted":
|
||||||
actual += self.get_amount(posting)
|
actual += self.get_amount(posting)
|
||||||
@@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report:
|
|||||||
qb.from_(inv_item)
|
qb.from_(inv_item)
|
||||||
.join(inv)
|
.join(inv)
|
||||||
.on(inv.name == inv_item.parent)
|
.on(inv.name == inv_item.parent)
|
||||||
.join(gle)
|
.left_join(gle)
|
||||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||||
.select(
|
.select(
|
||||||
inv.name.as_("doc"),
|
inv.name.as_("doc"),
|
||||||
|
|||||||
@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
|||||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||||
]
|
]
|
||||||
self.assertEqual(report.period_total, expected)
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||||
|
)
|
||||||
|
def test_zero_amount(self):
|
||||||
|
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||||
|
item = frappe.get_doc("Item", self.item)
|
||||||
|
item.enable_deferred_expense = 1
|
||||||
|
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||||
|
item.no_of_months_exp = 12
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
supplier=self.supplier,
|
||||||
|
is_return=False,
|
||||||
|
update_stock=False,
|
||||||
|
posting_date=frappe.utils.datetime.date(2021, 12, 30),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
do_not_save=True,
|
||||||
|
rate=3910,
|
||||||
|
price_list_rate=3910,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
qty=1,
|
||||||
|
)
|
||||||
|
pi.set_posting_time = True
|
||||||
|
pi.items[0].enable_deferred_expense = 1
|
||||||
|
pi.items[0].service_start_date = "2021-12-30"
|
||||||
|
pi.items[0].service_end_date = "2022-12-30"
|
||||||
|
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||||
|
pi.items[0].expense_account = self.expense_account
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pda = frappe.get_doc(
|
||||||
|
doctype="Process Deferred Accounting",
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2022-01-01",
|
||||||
|
end_date="2022-01-31",
|
||||||
|
type="Expense",
|
||||||
|
company=self.company,
|
||||||
|
)
|
||||||
|
pda.insert()
|
||||||
|
pda.submit()
|
||||||
|
|
||||||
|
# execute report
|
||||||
|
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"period_start_date": "2022-01-01",
|
||||||
|
"period_end_date": "2022-01-31",
|
||||||
|
"from_fiscal_year": fiscal_year.year,
|
||||||
|
"to_fiscal_year": fiscal_year.year,
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"type": "Expense",
|
||||||
|
"with_upcoming_postings": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||||
|
report.run()
|
||||||
|
|
||||||
|
# fetch the invoice from deferred invoices list
|
||||||
|
inv = [d for d in report.deferred_invoices if d.name == pi.name]
|
||||||
|
# make sure the list isn't empty
|
||||||
|
self.assertTrue(inv)
|
||||||
|
# calculate the total deferred expense for the period
|
||||||
|
inv = inv[0].calculate_invoice_revenue_expense_for_period()
|
||||||
|
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
|
||||||
|
# make sure the total deferred expense is greater than 0
|
||||||
|
self.assertLess(deferred_exp, 0)
|
||||||
|
|||||||
@@ -460,7 +460,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
|
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
group_by_value = gle.get(group_by)
|
group_by_value = gle.get(group_by)
|
||||||
gle.voucher_type = gle.voucher_type
|
|
||||||
gle.voucher_subtype = _(gle.voucher_subtype)
|
gle.voucher_subtype = _(gle.voucher_subtype)
|
||||||
gle.against_voucher_type = _(gle.against_voucher_type)
|
gle.against_voucher_type = _(gle.against_voucher_type)
|
||||||
gle.remarks = _(gle.remarks)
|
gle.remarks = _(gle.remarks)
|
||||||
|
|||||||
@@ -720,20 +720,22 @@ class GrossProfitGenerator:
|
|||||||
frappe.qb.from_(purchase_invoice_item)
|
frappe.qb.from_(purchase_invoice_item)
|
||||||
.inner_join(purchase_invoice)
|
.inner_join(purchase_invoice)
|
||||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||||
.select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor)
|
.select(
|
||||||
|
purchase_invoice.name,
|
||||||
|
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||||
|
)
|
||||||
.where(purchase_invoice.docstatus == 1)
|
.where(purchase_invoice.docstatus == 1)
|
||||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||||
.where(purchase_invoice_item.item_code == item_code)
|
.where(purchase_invoice_item.item_code == item_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
if row.project:
|
if row.project:
|
||||||
query.where(purchase_invoice_item.project == row.project)
|
query = query.where(purchase_invoice_item.project == row.project)
|
||||||
|
|
||||||
if row.cost_center:
|
if row.cost_center:
|
||||||
query.where(purchase_invoice_item.cost_center == row.cost_center)
|
query = query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||||
|
|
||||||
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
|
query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1)
|
||||||
query.limit(1)
|
|
||||||
last_purchase_rate = query.run()
|
last_purchase_rate = query.run()
|
||||||
|
|
||||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||||
|
|||||||
@@ -516,6 +516,10 @@ def reconcile_against_document(
|
|||||||
doc.make_advance_gl_entries()
|
doc.make_advance_gl_entries()
|
||||||
else:
|
else:
|
||||||
gl_map = doc.build_gl_map()
|
gl_map = doc.build_gl_map()
|
||||||
|
# Make sure there is no overallocation
|
||||||
|
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||||
|
|
||||||
|
process_debit_credit_difference(gl_map)
|
||||||
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
|
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
|
||||||
|
|
||||||
# Only update outstanding for newly linked vouchers
|
# Only update outstanding for newly linked vouchers
|
||||||
|
|||||||
@@ -1738,12 +1738,12 @@ def create_asset(**args):
|
|||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
def create_asset_category():
|
def create_asset_category(enable_cwip=1):
|
||||||
asset_category = frappe.new_doc("Asset Category")
|
asset_category = frappe.new_doc("Asset Category")
|
||||||
asset_category.asset_category_name = "Computers"
|
asset_category.asset_category_name = "Computers"
|
||||||
asset_category.total_number_of_depreciations = 3
|
asset_category.total_number_of_depreciations = 3
|
||||||
asset_category.frequency_of_depreciation = 3
|
asset_category.frequency_of_depreciation = 3
|
||||||
asset_category.enable_cwip_accounting = 1
|
asset_category.enable_cwip_accounting = enable_cwip
|
||||||
asset_category.append(
|
asset_category.append(
|
||||||
"accounts",
|
"accounts",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -772,12 +772,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
else:
|
else:
|
||||||
account = frappe.db.get_value(
|
account = frappe.get_doc("Account", {"account_name": account_name, "company": company})
|
||||||
"Account",
|
|
||||||
filters={"account_name": account_name, "company": company},
|
|
||||||
fieldname="name",
|
|
||||||
pluck=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
@@ -808,22 +803,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
|
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||||
|
|
||||||
pi = make_purchase_invoice(po_doc.name)
|
|
||||||
pi.append(
|
|
||||||
"advances",
|
|
||||||
{
|
|
||||||
"reference_type": pe.doctype,
|
|
||||||
"reference_name": pe.name,
|
|
||||||
"reference_row": pe.references[0].name,
|
|
||||||
"advance_amount": 5000,
|
|
||||||
"allocated_amount": 5000,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
pi.save().submit()
|
|
||||||
pe.reload()
|
|
||||||
po_doc.reload()
|
|
||||||
self.assertEqual(po_doc.advance_paid, 0)
|
|
||||||
|
|
||||||
company_doc.book_advance_payments_in_separate_party_account = False
|
company_doc.book_advance_payments_in_separate_party_account = False
|
||||||
company_doc.save()
|
company_doc.save()
|
||||||
|
|
||||||
|
|||||||
@@ -406,7 +406,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contact_and_address_tab",
|
"fieldname": "contact_and_address_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact & Address"
|
"label": "Address & Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "accounting_tab",
|
"fieldname": "accounting_tab",
|
||||||
@@ -485,7 +485,7 @@
|
|||||||
"link_fieldname": "party"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:10:48.412732",
|
"modified": "2024-05-08 18:02:57.342931",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier",
|
"name": "Supplier",
|
||||||
|
|||||||
@@ -778,6 +778,9 @@ class AccountsController(TransactionBase):
|
|||||||
# reset pricing rule fields if pricing_rule_removed
|
# reset pricing rule fields if pricing_rule_removed
|
||||||
item.set(fieldname, value)
|
item.set(fieldname, value)
|
||||||
|
|
||||||
|
elif fieldname == "expense_account" and not item.get("expense_account"):
|
||||||
|
item.expense_account = value
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
||||||
"is_fixed_asset"
|
"is_fixed_asset"
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -712,6 +712,7 @@ class BuyingController(SubcontractingController):
|
|||||||
def auto_make_assets(self, asset_items):
|
def auto_make_assets(self, asset_items):
|
||||||
items_data = get_asset_item_details(asset_items)
|
items_data = get_asset_item_details(asset_items)
|
||||||
messages = []
|
messages = []
|
||||||
|
alert = False
|
||||||
|
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.is_fixed_asset:
|
if d.is_fixed_asset:
|
||||||
@@ -761,9 +762,10 @@ class BuyingController(SubcontractingController):
|
|||||||
frappe.bold(d.item_code)
|
frappe.bold(d.item_code)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
alert = True
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
frappe.msgprint(message, title="Success", indicator="green")
|
frappe.msgprint(message, title="Success", indicator="green", alert=alert)
|
||||||
|
|
||||||
def make_asset(self, row, is_grouped_asset=False):
|
def make_asset(self, row, is_grouped_asset=False):
|
||||||
if not row.asset_location:
|
if not row.asset_location:
|
||||||
|
|||||||
@@ -1227,8 +1227,8 @@ def get_accounting_ledger_preview(doc, filters):
|
|||||||
"debit",
|
"debit",
|
||||||
"credit",
|
"credit",
|
||||||
"against",
|
"against",
|
||||||
"party",
|
|
||||||
"party_type",
|
"party_type",
|
||||||
|
"party",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"against_voucher_type",
|
"against_voucher_type",
|
||||||
"against_voucher",
|
"against_voucher",
|
||||||
@@ -1404,7 +1404,12 @@ def is_reposting_pending():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def future_sle_exists(args, sl_entries=None):
|
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||||
|
if allow_force_reposting and frappe.db.get_single_value(
|
||||||
|
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
key = (args.voucher_type, args.voucher_no)
|
key = (args.voucher_type, args.voucher_no)
|
||||||
if not hasattr(frappe.local, "future_sle"):
|
if not hasattr(frappe.local, "future_sle"):
|
||||||
frappe.local.future_sle = {}
|
frappe.local.future_sle = {}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ def send_mail(entry, email_campaign):
|
|||||||
doctype="Email Campaign",
|
doctype="Email Campaign",
|
||||||
name=email_campaign.name,
|
name=email_campaign.name,
|
||||||
subject=frappe.render_template(email_template.get("subject"), context),
|
subject=frappe.render_template(email_template.get("subject"), context),
|
||||||
content=frappe.render_template(email_template.get("response"), context),
|
content=frappe.render_template(email_template.response_, context),
|
||||||
sender=sender,
|
sender=sender,
|
||||||
recipients=recipient_list,
|
recipients=recipient_list,
|
||||||
communication_medium="Email",
|
communication_medium="Email",
|
||||||
|
|||||||
2103
erpnext/locale/ar.po
2103
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
4774
erpnext/locale/bs.po
4774
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2525
erpnext/locale/de.po
2525
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2103
erpnext/locale/eo.po
2103
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2611
erpnext/locale/es.po
2611
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3419
erpnext/locale/fa.po
3419
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2103
erpnext/locale/fr.po
2103
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
8768
erpnext/locale/tr.po
8768
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
@@ -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:
|
if not self.data:
|
||||||
return
|
return
|
||||||
|
|
||||||
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
|
total_row = {"item_code": _("Total Quantity")}
|
||||||
|
|
||||||
for value in self.data:
|
for value in self.data:
|
||||||
for period in self.period_list:
|
for period in self.period_list:
|
||||||
|
|||||||
@@ -363,4 +363,5 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
|||||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||||
|
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
# update debit and credit in transaction currency:
|
||||||
|
# if transaction currency is same as account currency,
|
||||||
|
# then debit and credit in transaction currency is same as debit and credit in account currency
|
||||||
|
# else debit and credit divided by exchange rate
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
|
frappe.db.sql(
|
||||||
|
"""
|
||||||
|
UPDATE `tabGL Entry`
|
||||||
|
SET
|
||||||
|
debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate),
|
||||||
|
credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate)
|
||||||
|
WHERE
|
||||||
|
transaction_exchange_rate > 0
|
||||||
|
and transaction_currency is not null
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -22,6 +22,7 @@ erpnext.accounts.dimensions = {
|
|||||||
});
|
});
|
||||||
me.default_dimensions = r.message[1];
|
me.default_dimensions = r.message[1];
|
||||||
me.setup_filters(frm, doctype);
|
me.setup_filters(frm, doctype);
|
||||||
|
me.update_dimension(frm, doctype);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ erpnext.sales_common = {
|
|||||||
frappe.model.set_value(item.doctype, item.name, {
|
frappe.model.set_value(item.doctype, item.name, {
|
||||||
serial_and_batch_bundle: r.name,
|
serial_and_batch_bundle: r.name,
|
||||||
use_serial_batch_fields: 0,
|
use_serial_batch_fields: 0,
|
||||||
|
incoming_rate: r.avg_rate,
|
||||||
qty:
|
qty:
|
||||||
qty /
|
qty /
|
||||||
flt(
|
flt(
|
||||||
|
|||||||
@@ -482,7 +482,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "contact_and_address_tab",
|
"fieldname": "contact_and_address_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Contact & Address"
|
"label": "Address & Contact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "defaults_tab",
|
"fieldname": "defaults_tab",
|
||||||
@@ -583,7 +583,7 @@
|
|||||||
"link_fieldname": "party"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:06:48.056107",
|
"modified": "2024-05-08 18:03:20.716169",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Customer",
|
"name": "Customer",
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ frappe.ui.form.on("Quotation", {
|
|||||||
frm.trigger("set_label");
|
frm.trigger("set_label");
|
||||||
frm.trigger("toggle_reqd_lead_customer");
|
frm.trigger("toggle_reqd_lead_customer");
|
||||||
frm.trigger("set_dynamic_field_label");
|
frm.trigger("set_dynamic_field_label");
|
||||||
|
frm.set_value("party_name", "");
|
||||||
|
frm.set_value("customer_name", "");
|
||||||
},
|
},
|
||||||
|
|
||||||
set_label: function (frm) {
|
set_label: function (frm) {
|
||||||
@@ -97,7 +99,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
frappe.dynamic_link = {
|
frappe.dynamic_link = {
|
||||||
doc: this.frm.doc,
|
doc: this.frm.doc,
|
||||||
fieldname: "party_name",
|
fieldname: "party_name",
|
||||||
doctype: doc.quotation_to == "Customer" ? "Customer" : "Lead",
|
doctype: doc.quotation_to,
|
||||||
};
|
};
|
||||||
|
|
||||||
var me = this;
|
var me = this;
|
||||||
@@ -197,6 +199,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
};
|
};
|
||||||
} else if (this.frm.doc.quotation_to == "Prospect") {
|
} else if (this.frm.doc.quotation_to == "Prospect") {
|
||||||
this.frm.set_df_property("party_name", "label", "Prospect");
|
this.frm.set_df_property("party_name", "label", "Prospect");
|
||||||
|
this.frm.fields_dict.party_name.get_query = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ def update_qty(bin_name, args):
|
|||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
# actual qty is not up to date in case of backdated transaction
|
# actual qty is not up to date in case of backdated transaction
|
||||||
if future_sle_exists(args):
|
if future_sle_exists(args, allow_force_reposting=False):
|
||||||
last_sle_qty = (
|
last_sle_qty = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.select(sle.qty_after_transaction)
|
.select(sle.qty_after_transaction)
|
||||||
|
|||||||
@@ -124,13 +124,14 @@
|
|||||||
"per_returned",
|
"per_returned",
|
||||||
"transporter_info",
|
"transporter_info",
|
||||||
"transporter",
|
"transporter",
|
||||||
"driver",
|
|
||||||
"lr_no",
|
"lr_no",
|
||||||
"vehicle_no",
|
"delivery_trip",
|
||||||
|
"driver",
|
||||||
"col_break34",
|
"col_break34",
|
||||||
"transporter_name",
|
"transporter_name",
|
||||||
"driver_name",
|
|
||||||
"lr_date",
|
"lr_date",
|
||||||
|
"vehicle_no",
|
||||||
|
"driver_name",
|
||||||
"customer_po_details",
|
"customer_po_details",
|
||||||
"po_no",
|
"po_no",
|
||||||
"column_break_17",
|
"column_break_17",
|
||||||
@@ -1391,13 +1392,20 @@
|
|||||||
"fieldname": "named_place",
|
"fieldname": "named_place",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Named Place"
|
"label": "Named Place"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "delivery_trip",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Delivery Trip",
|
||||||
|
"options": "Delivery Trip",
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-truck",
|
"icon": "fa fa-truck",
|
||||||
"idx": 146,
|
"idx": 146,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:49.519676",
|
"modified": "2024-03-29 19:03:55.374173",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note",
|
"name": "Delivery Note",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class DeliveryNote(SellingController):
|
|||||||
customer_address: DF.Link | None
|
customer_address: DF.Link | None
|
||||||
customer_group: DF.Link | None
|
customer_group: DF.Link | None
|
||||||
customer_name: DF.Data | None
|
customer_name: DF.Data | None
|
||||||
|
delivery_trip: DF.Link | None
|
||||||
disable_rounded_total: DF.Check
|
disable_rounded_total: DF.Check
|
||||||
discount_amount: DF.Currency
|
discount_amount: DF.Currency
|
||||||
dispatch_address: DF.TextEditor | None
|
dispatch_address: DF.TextEditor | None
|
||||||
@@ -76,7 +77,7 @@ class DeliveryNote(SellingController):
|
|||||||
ignore_pricing_rule: DF.Check
|
ignore_pricing_rule: DF.Check
|
||||||
in_words: DF.Data | None
|
in_words: DF.Data | None
|
||||||
incoterm: DF.Link | None
|
incoterm: DF.Link | None
|
||||||
installation_status: DF.Literal[None]
|
installation_status: DF.LiteralNone
|
||||||
instructions: DF.Text | None
|
instructions: DF.Text | None
|
||||||
inter_company_reference: DF.Link | None
|
inter_company_reference: DF.Link | None
|
||||||
is_internal_customer: DF.Check
|
is_internal_customer: DF.Check
|
||||||
@@ -1066,7 +1067,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_delivery_trip(source_name, target_doc=None):
|
def make_delivery_trip(source_name, target_doc=None, kwargs=None):
|
||||||
if not target_doc:
|
if not target_doc:
|
||||||
target_doc = frappe.new_doc("Delivery Trip")
|
target_doc = frappe.new_doc("Delivery Trip")
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
@@ -1075,7 +1076,6 @@ def make_delivery_trip(source_name, target_doc=None):
|
|||||||
{
|
{
|
||||||
"Delivery Note": {
|
"Delivery Note": {
|
||||||
"doctype": "Delivery Stop",
|
"doctype": "Delivery Stop",
|
||||||
"validation": {"docstatus": ["=", 1]},
|
|
||||||
"on_parent": target_doc,
|
"on_parent": target_doc,
|
||||||
"field_map": {
|
"field_map": {
|
||||||
"name": "delivery_note",
|
"name": "delivery_note",
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ frappe.listview_settings["Delivery Note"] = {
|
|||||||
const docnames = doclist.get_checked_items(true);
|
const docnames = doclist.get_checked_items(true);
|
||||||
|
|
||||||
if (selected_docs.length > 0) {
|
if (selected_docs.length > 0) {
|
||||||
for (let doc of selected_docs) {
|
|
||||||
if (!doc.docstatus) {
|
|
||||||
frappe.throw(__("Cannot create a Delivery Trip from Draft documents."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.new_doc("Delivery Trip").then(() => {
|
frappe.new_doc("Delivery Trip").then(() => {
|
||||||
// Empty out the child table before inserting new ones
|
// Empty out the child table before inserting new ones
|
||||||
cur_frm.set_value("delivery_stops", []);
|
cur_frm.set_value("delivery_stops", []);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from erpnext.stock.doctype.delivery_note.delivery_note import (
|
|||||||
make_delivery_trip,
|
make_delivery_trip,
|
||||||
make_sales_invoice,
|
make_sales_invoice,
|
||||||
)
|
)
|
||||||
|
from erpnext.stock.doctype.delivery_trip.test_delivery_trip import create_driver
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||||
@@ -1064,6 +1065,21 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
dn = create_delivery_note()
|
dn = create_delivery_note()
|
||||||
dt = make_delivery_trip(dn.name)
|
dt = make_delivery_trip(dn.name)
|
||||||
self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note)
|
self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note)
|
||||||
|
dt.delivery_stops[0].customer_address = "fake string"
|
||||||
|
dt.flags.ignore_mandatory = True
|
||||||
|
dt.save()
|
||||||
|
dn.reload()
|
||||||
|
self.assertEqual(dn.delivery_trip, dt.name)
|
||||||
|
|
||||||
|
dn = create_delivery_note(do_not_submit=True)
|
||||||
|
dt = make_delivery_trip(dn.name)
|
||||||
|
self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note)
|
||||||
|
dt.driver = create_driver()
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
r"^Delivery Notes should not be in draft state when submitting a Delivery Trip.*",
|
||||||
|
dt.submit,
|
||||||
|
)
|
||||||
|
|
||||||
def test_delivery_note_with_cost_center(self):
|
def test_delivery_note_with_cost_center(self):
|
||||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ frappe.ui.form.on("Delivery Trip", {
|
|||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
},
|
},
|
||||||
get_query_filters: {
|
get_query_filters: {
|
||||||
docstatus: 1,
|
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,11 +54,18 @@ class DeliveryTrip(Document):
|
|||||||
if self._action == "submit" and not self.driver:
|
if self._action == "submit" and not self.driver:
|
||||||
frappe.throw(_("A driver must be set to submit."))
|
frappe.throw(_("A driver must be set to submit."))
|
||||||
|
|
||||||
|
if self._action == "submit":
|
||||||
|
self.validate_delivery_note_not_draft()
|
||||||
self.validate_stop_addresses()
|
self.validate_stop_addresses()
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
self.update_delivery_notes()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
self.update_delivery_notes(delete=True)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.update_delivery_notes()
|
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
self.update_status()
|
self.update_status()
|
||||||
@@ -72,6 +79,20 @@ class DeliveryTrip(Document):
|
|||||||
if not stop.customer_address:
|
if not stop.customer_address:
|
||||||
stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict())
|
stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict())
|
||||||
|
|
||||||
|
def validate_delivery_note_not_draft(self):
|
||||||
|
delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note))
|
||||||
|
draft_delivery_notes = frappe.get_all(
|
||||||
|
"Delivery Note",
|
||||||
|
{"docstatus": 0, "name": ["in", delivery_notes]},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
if draft_delivery_notes:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Delivery Notes should not be in draft state when submitting a Delivery Trip. The following Delivery Notes are still in draft state: {0}. Please submit them first."
|
||||||
|
).format(", ".join(draft_delivery_notes))
|
||||||
|
)
|
||||||
|
|
||||||
def update_status(self):
|
def update_status(self):
|
||||||
status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[self.docstatus]
|
status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[self.docstatus]
|
||||||
|
|
||||||
@@ -100,22 +121,29 @@ class DeliveryTrip(Document):
|
|||||||
"driver": self.driver,
|
"driver": self.driver,
|
||||||
"driver_name": self.driver_name,
|
"driver_name": self.driver_name,
|
||||||
"vehicle_no": self.vehicle,
|
"vehicle_no": self.vehicle,
|
||||||
|
"delivery_trip": self.name,
|
||||||
"lr_no": self.name,
|
"lr_no": self.name,
|
||||||
"lr_date": self.departure_time,
|
"lr_date": self.departure_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delivery_notes_updated = set()
|
||||||
|
|
||||||
for delivery_note in delivery_notes:
|
for delivery_note in delivery_notes:
|
||||||
note_doc = frappe.get_doc("Delivery Note", delivery_note)
|
note_doc = frappe.get_doc("Delivery Note", delivery_note)
|
||||||
|
|
||||||
for field, value in update_fields.items():
|
for field, value in update_fields.items():
|
||||||
|
prev_value = getattr(note_doc, field)
|
||||||
value = None if delete else value
|
value = None if delete else value
|
||||||
|
if prev_value != value:
|
||||||
|
delivery_notes_updated.add(delivery_note)
|
||||||
setattr(note_doc, field, value)
|
setattr(note_doc, field, value)
|
||||||
|
|
||||||
note_doc.flags.ignore_validate_update_after_submit = True
|
if delivery_note in delivery_notes_updated:
|
||||||
note_doc.save()
|
note_doc.flags.ignore_validate_update_after_submit = True
|
||||||
|
note_doc.save()
|
||||||
|
|
||||||
delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
|
delivery_notes_updated = [get_link_to_form("Delivery Note", note) for note in delivery_notes_updated]
|
||||||
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes)))
|
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes_updated)))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def process_route(self, optimize):
|
def process_route(self, optimize):
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ frappe.ui.form.on("Item", {
|
|||||||
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
|
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
|
||||||
|
|
||||||
frm.make_methods = {
|
frm.make_methods = {
|
||||||
|
Quotation: () => {
|
||||||
|
open_form(frm, "Quotation", "Quotation Item", "items");
|
||||||
|
},
|
||||||
"Sales Order": () => {
|
"Sales Order": () => {
|
||||||
open_form(frm, "Sales Order", "Sales Order Item", "items");
|
open_form(frm, "Sales Order", "Sales Order Item", "items");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
"section_break_11",
|
"section_break_11",
|
||||||
"description",
|
"description",
|
||||||
"brand",
|
"brand",
|
||||||
|
"unit_of_measure_conversion",
|
||||||
|
"uoms",
|
||||||
"dashboard_tab",
|
"dashboard_tab",
|
||||||
"inventory_section",
|
"inventory_section",
|
||||||
"inventory_settings_section",
|
"inventory_settings_section",
|
||||||
@@ -52,8 +54,6 @@
|
|||||||
"barcodes",
|
"barcodes",
|
||||||
"reorder_section",
|
"reorder_section",
|
||||||
"reorder_levels",
|
"reorder_levels",
|
||||||
"unit_of_measure_conversion",
|
|
||||||
"uoms",
|
|
||||||
"serial_nos_and_batches",
|
"serial_nos_and_batches",
|
||||||
"has_batch_no",
|
"has_batch_no",
|
||||||
"create_new_batch",
|
"create_new_batch",
|
||||||
@@ -891,7 +891,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-03-27 13:09:53.380997",
|
"modified": "2024-04-30 13:46:39.098753",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -65,15 +65,13 @@ class Item(Document):
|
|||||||
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
|
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
|
||||||
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
|
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
|
||||||
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
|
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
|
||||||
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import (
|
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ItemVariantAttribute
|
||||||
ItemVariantAttribute,
|
|
||||||
)
|
|
||||||
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
|
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
|
||||||
|
|
||||||
allow_alternative_item: DF.Check
|
allow_alternative_item: DF.Check
|
||||||
allow_negative_stock: DF.Check
|
allow_negative_stock: DF.Check
|
||||||
asset_category: DF.Link | None
|
asset_category: DF.Link | None
|
||||||
asset_naming_series: DF.Literal
|
asset_naming_series: DF.Literal[None]
|
||||||
attributes: DF.Table[ItemVariantAttribute]
|
attributes: DF.Table[ItemVariantAttribute]
|
||||||
auto_create_assets: DF.Check
|
auto_create_assets: DF.Check
|
||||||
barcodes: DF.Table[ItemBarcode]
|
barcodes: DF.Table[ItemBarcode]
|
||||||
|
|||||||
@@ -156,6 +156,33 @@ class TestItem(FrappeTestCase):
|
|||||||
for key, value in to_check.items():
|
for key, value in to_check.items():
|
||||||
self.assertEqual(value, details.get(key), key)
|
self.assertEqual(value, details.get(key), key)
|
||||||
|
|
||||||
|
def test_get_asset_item_details(self):
|
||||||
|
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
||||||
|
|
||||||
|
create_asset_category(0)
|
||||||
|
create_fixed_asset_item()
|
||||||
|
|
||||||
|
details = get_item_details(
|
||||||
|
{
|
||||||
|
"item_code": "Macbook Pro",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"currency": "INR",
|
||||||
|
"doctype": "Purchase Receipt",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(details.get("expense_account"), "_Test Fixed Asset - _TC")
|
||||||
|
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", "1")
|
||||||
|
details = get_item_details(
|
||||||
|
{
|
||||||
|
"item_code": "Macbook Pro",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"currency": "INR",
|
||||||
|
"doctype": "Purchase Receipt",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(details.get("expense_account"), "CWIP Account - _TC")
|
||||||
|
|
||||||
def test_item_tax_template(self):
|
def test_item_tax_template(self):
|
||||||
expected_item_tax_template = [
|
expected_item_tax_template = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,10 +52,13 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "item_code.stock_uom",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "uom",
|
"fieldname": "uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "UOM",
|
"label": "UOM",
|
||||||
"options": "UOM"
|
"options": "UOM",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -221,7 +224,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:09:54.930834",
|
"modified": "2024-04-02 22:18:00.450641",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item Price",
|
"name": "Item Price",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ItemPrice(Document):
|
|||||||
reference: DF.Data | None
|
reference: DF.Data | None
|
||||||
selling: DF.Check
|
selling: DF.Check
|
||||||
supplier: DF.Link | None
|
supplier: DF.Link | None
|
||||||
uom: DF.Link | None
|
uom: DF.Link
|
||||||
valid_from: DF.Date | None
|
valid_from: DF.Date | None
|
||||||
valid_upto: DF.Date | None
|
valid_upto: DF.Date | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -790,7 +790,7 @@ def get_available_item_locations(
|
|||||||
locations = get_locations_based_on_required_qty(locations, required_qty)
|
locations = get_locations_based_on_required_qty(locations, required_qty)
|
||||||
|
|
||||||
if not ignore_validation:
|
if not ignore_validation:
|
||||||
validate_picked_materials(item_code, required_qty, locations)
|
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
|
||||||
|
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
@@ -810,7 +810,7 @@ def get_locations_based_on_required_qty(locations, required_qty):
|
|||||||
return filtered_locations
|
return filtered_locations
|
||||||
|
|
||||||
|
|
||||||
def validate_picked_materials(item_code, required_qty, locations):
|
def validate_picked_materials(item_code, required_qty, locations, picked_item_details=None):
|
||||||
for location in list(locations):
|
for location in list(locations):
|
||||||
if location["qty"] < 0:
|
if location["qty"] < 0:
|
||||||
locations.remove(location)
|
locations.remove(location)
|
||||||
@@ -819,15 +819,25 @@ def validate_picked_materials(item_code, required_qty, locations):
|
|||||||
remaining_qty = required_qty - total_qty_available
|
remaining_qty = required_qty - total_qty_available
|
||||||
|
|
||||||
if remaining_qty > 0:
|
if remaining_qty > 0:
|
||||||
frappe.msgprint(
|
if picked_item_details:
|
||||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
frappe.msgprint(
|
||||||
remaining_qty, get_link_to_form("Item", item_code)
|
_("{0} units of Item {1} is picked in another Pick List.").format(
|
||||||
),
|
remaining_qty, get_link_to_form("Item", item_code)
|
||||||
title=_("Already Picked"),
|
),
|
||||||
)
|
title=_("Already Picked"),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("{0} units of Item {1} is not available in any of the warehouses.").format(
|
||||||
|
remaining_qty, get_link_to_form("Item", item_code)
|
||||||
|
),
|
||||||
|
title=_("Insufficient Stock"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
||||||
|
filterd_locations = []
|
||||||
for row in locations:
|
for row in locations:
|
||||||
key = row.warehouse
|
key = row.warehouse
|
||||||
if row.batch_no:
|
if row.batch_no:
|
||||||
@@ -835,6 +845,7 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
|||||||
|
|
||||||
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
||||||
if not picked_qty:
|
if not picked_qty:
|
||||||
|
filterd_locations.append(row)
|
||||||
continue
|
continue
|
||||||
if picked_qty > row.qty:
|
if picked_qty > row.qty:
|
||||||
row.qty = 0
|
row.qty = 0
|
||||||
@@ -845,7 +856,10 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
|||||||
if row.serial_nos:
|
if row.serial_nos:
|
||||||
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
|
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
|
||||||
|
|
||||||
return locations
|
if row.qty > 0:
|
||||||
|
filterd_locations.append(row)
|
||||||
|
|
||||||
|
return filterd_locations
|
||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_serial_and_batched_item(
|
def get_available_item_locations_for_serial_and_batched_item(
|
||||||
|
|||||||
@@ -977,3 +977,157 @@ class TestPickList(FrappeTestCase):
|
|||||||
so = make_sales_order(item_code=item, qty=4, rate=100)
|
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||||
pl = create_pick_list(so.name)
|
pl = create_pick_list(so.name)
|
||||||
self.assertFalse(hasattr(pl, "locations"))
|
self.assertFalse(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_validation_for_multiple_batches_and_sales_order(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Batch Pick List Item For Multiple Batches",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=6, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 5.0)
|
||||||
|
self.assertEqual(pl1.locations[1].qty, 1.0)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=4, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_for_multiple_sales_order_with_multiple_batches(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Batch Pick List Item For Multiple Batches and Sales Order",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||||
|
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||||
|
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Serial No Pick List Item For Multiple Batches and Sales Order",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 10)
|
||||||
|
|
||||||
|
serial_nos = pl1.locations[0].serial_no.split("\n")
|
||||||
|
self.assertEqual(len(serial_nos), 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
new_serial_nos = pl.locations[0].serial_no.split("\n")
|
||||||
|
self.assertEqual(len(new_serial_nos), 110)
|
||||||
|
|
||||||
|
for sn in serial_nos:
|
||||||
|
self.assertFalse(sn in new_serial_nos)
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Non Serialized Pick List Item For Multiple Batches and Sales Order",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
|
||||||
|
self.assertEqual(pl.locations[0].qty, 80.0)
|
||||||
|
|||||||
@@ -132,7 +132,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item",
|
"label": "Item",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "quantity_section",
|
"fieldname": "quantity_section",
|
||||||
@@ -240,7 +241,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:13.391216",
|
"modified": "2024-05-07 15:32:42.905446",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
|||||||
@@ -671,19 +671,8 @@ class PurchaseReceipt(BuyingController):
|
|||||||
else self.get_company_default("stock_received_but_not_billed")
|
else self.get_company_default("stock_received_but_not_billed")
|
||||||
)
|
)
|
||||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||||
|
|
||||||
if d.is_fixed_asset:
|
if d.is_fixed_asset:
|
||||||
if is_cwip_accounting_enabled(d.asset_category):
|
stock_asset_account_name = d.expense_account
|
||||||
stock_asset_account_name = get_asset_account(
|
|
||||||
"capital_work_in_progress_account",
|
|
||||||
asset_category=d.asset_category,
|
|
||||||
company=self.company,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stock_asset_account_name = get_asset_category_account(
|
|
||||||
"fixed_asset_account", asset_category=d.asset_category, company=self.company
|
|
||||||
)
|
|
||||||
|
|
||||||
stock_value_diff = (
|
stock_value_diff = (
|
||||||
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
|
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1149,7 +1149,18 @@ def make_batch_nos(item_code, batch_nos):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
batch_nos_details.append(
|
batch_nos_details.append(
|
||||||
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
|
(
|
||||||
|
batch_no,
|
||||||
|
batch_no,
|
||||||
|
now(),
|
||||||
|
now(),
|
||||||
|
user,
|
||||||
|
user,
|
||||||
|
item.item_code,
|
||||||
|
item.item_name,
|
||||||
|
item.description,
|
||||||
|
1,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
@@ -1162,6 +1173,7 @@ def make_batch_nos(item_code, batch_nos):
|
|||||||
"item",
|
"item",
|
||||||
"item_name",
|
"item_name",
|
||||||
"description",
|
"description",
|
||||||
|
"use_batchwise_valuation",
|
||||||
]
|
]
|
||||||
|
|
||||||
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
|
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
|
||||||
|
|||||||
@@ -498,6 +498,8 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
|
|
||||||
make_batch_nos(item_code, batch_nos)
|
make_batch_nos(item_code, batch_nos)
|
||||||
self.assertTrue(frappe.db.exists("Batch", batch_id))
|
self.assertTrue(frappe.db.exists("Batch", batch_id))
|
||||||
|
use_batchwise_valuation = frappe.db.get_value("Batch", batch_id, "use_batchwise_valuation")
|
||||||
|
self.assertEqual(use_batchwise_valuation, 1)
|
||||||
|
|
||||||
batch_id = "TEST-BATTCCH-VAL-00001"
|
batch_id = "TEST-BATTCCH-VAL-00001"
|
||||||
batch_nos = [{"batch_no": batch_id, "qty": 1}]
|
batch_nos = [{"batch_no": batch_id, "qty": 1}]
|
||||||
|
|||||||
@@ -1340,6 +1340,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
|
|||||||
frappe.model.set_value(item.doctype, item.name, {
|
frappe.model.set_value(item.doctype, item.name, {
|
||||||
serial_and_batch_bundle: r.name,
|
serial_and_batch_bundle: r.name,
|
||||||
use_serial_batch_fields: 0,
|
use_serial_batch_fields: 0,
|
||||||
|
basic_rate: r.avg_rate,
|
||||||
qty:
|
qty:
|
||||||
Math.abs(r.total_qty) /
|
Math.abs(r.total_qty) /
|
||||||
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
|
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"end_time",
|
"end_time",
|
||||||
"limits_dont_apply_on",
|
"limits_dont_apply_on",
|
||||||
"item_based_reposting",
|
"item_based_reposting",
|
||||||
|
"do_reposting_for_each_stock_transaction",
|
||||||
"errors_notification_section",
|
"errors_notification_section",
|
||||||
"notify_reposting_error_to_role"
|
"notify_reposting_error_to_role"
|
||||||
],
|
],
|
||||||
@@ -65,12 +66,18 @@
|
|||||||
"fieldname": "errors_notification_section",
|
"fieldname": "errors_notification_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Errors Notification"
|
"label": "Errors Notification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "do_reposting_for_each_stock_transaction",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Do reposting for each Stock Transaction"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:45.069645",
|
"modified": "2024-04-24 12:19:40.204888",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reposting Settings",
|
"name": "Stock Reposting Settings",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class StockRepostingSettings(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
|
do_reposting_for_each_stock_transaction: DF.Check
|
||||||
end_time: DF.Time | None
|
end_time: DF.Time | None
|
||||||
item_based_reposting: DF.Check
|
item_based_reposting: DF.Check
|
||||||
limit_reposting_timeslot: DF.Check
|
limit_reposting_timeslot: DF.Check
|
||||||
@@ -29,6 +30,10 @@ class StockRepostingSettings(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.set_minimum_reposting_time_slot()
|
self.set_minimum_reposting_time_slot()
|
||||||
|
|
||||||
|
def before_save(self):
|
||||||
|
if self.do_reposting_for_each_stock_transaction:
|
||||||
|
self.item_based_reposting = 1
|
||||||
|
|
||||||
def set_minimum_reposting_time_slot(self):
|
def set_minimum_reposting_time_slot(self):
|
||||||
"""Ensure that timeslot for reposting is at least 12 hours."""
|
"""Ensure that timeslot for reposting is at least 12 hours."""
|
||||||
if not self.limit_reposting_timeslot:
|
if not self.limit_reposting_timeslot:
|
||||||
|
|||||||
@@ -38,3 +38,51 @@ class TestStockRepostingSettings(unittest.TestCase):
|
|||||||
|
|
||||||
users = get_recipients()
|
users = get_recipients()
|
||||||
self.assertTrue(user in users)
|
self.assertTrue(user in users)
|
||||||
|
|
||||||
|
def test_do_reposting_for_each_stock_transaction(self):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
|
||||||
|
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||||
|
|
||||||
|
item = make_item(
|
||||||
|
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
|
||||||
|
).name
|
||||||
|
|
||||||
|
stock_entry = make_stock_entry(
|
||||||
|
item_code=item,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
stock_entry_type="Material Receipt",
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||||
|
self.assertTrue(riv)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||||
|
|
||||||
|
def test_do_not_reposting_for_each_stock_transaction(self):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||||
|
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||||
|
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||||
|
|
||||||
|
item = make_item(
|
||||||
|
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
|
||||||
|
).name
|
||||||
|
|
||||||
|
stock_entry = make_stock_entry(
|
||||||
|
item_code=item,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
stock_entry_type="Material Receipt",
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||||
|
self.assertFalse(riv)
|
||||||
|
|||||||
@@ -418,7 +418,7 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval: doc.enable_stock_reservation",
|
"depends_on": "eval: doc.enable_stock_reservation",
|
||||||
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Receipt for Sales Order.",
|
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Request for Sales Order.",
|
||||||
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
|
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto Reserve Stock for Sales Order on Purchase"
|
"label": "Auto Reserve Stock for Sales Order on Purchase"
|
||||||
@@ -469,4 +469,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
|||||||
args["bill_date"] = doc.get("bill_date")
|
args["bill_date"] = doc.get("bill_date")
|
||||||
|
|
||||||
out = get_basic_details(args, item, overwrite_warehouse)
|
out = get_basic_details(args, item, overwrite_warehouse)
|
||||||
|
|
||||||
get_item_tax_template(args, item, out)
|
get_item_tax_template(args, item, out)
|
||||||
out["item_tax_rate"] = get_item_tax_map(
|
out["item_tax_rate"] = get_item_tax_map(
|
||||||
args.company,
|
args.company,
|
||||||
@@ -293,12 +292,26 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
|||||||
|
|
||||||
expense_account = None
|
expense_account = None
|
||||||
|
|
||||||
if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset:
|
if item.is_fixed_asset:
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||||
|
|
||||||
expense_account = get_asset_category_account(
|
if is_cwip_accounting_enabled(item.asset_category):
|
||||||
fieldname="fixed_asset_account", item=args.item_code, company=args.company
|
expense_account = get_asset_account(
|
||||||
)
|
"capital_work_in_progress_account",
|
||||||
|
asset_category=item.asset_category,
|
||||||
|
company=args.company,
|
||||||
|
)
|
||||||
|
elif args.get("doctype") in (
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Purchase Receipt",
|
||||||
|
"Purchase Order",
|
||||||
|
"Material Request",
|
||||||
|
):
|
||||||
|
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
|
|
||||||
|
expense_account = get_asset_category_account(
|
||||||
|
fieldname="fixed_asset_account", item=args.item_code, company=args.company
|
||||||
|
)
|
||||||
|
|
||||||
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
|
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
|
||||||
if not args.get("uom"):
|
if not args.get("uom"):
|
||||||
@@ -840,7 +853,12 @@ def insert_item_price(args):
|
|||||||
|
|
||||||
item_price = frappe.db.get_value(
|
item_price = frappe.db.get_value(
|
||||||
"Item Price",
|
"Item Price",
|
||||||
{"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency},
|
{
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"price_list": args.price_list,
|
||||||
|
"currency": args.currency,
|
||||||
|
"uom": args.stock_uom,
|
||||||
|
},
|
||||||
["name", "price_list_rate"],
|
["name", "price_list_rate"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,16 +40,25 @@ frappe.query_reports["Batch-Wise Balance History"] = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "warehouse_type",
|
||||||
|
label: __("Warehouse Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
width: "80",
|
||||||
|
options: "Warehouse Type",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldname: "warehouse",
|
fieldname: "warehouse",
|
||||||
label: __("Warehouse"),
|
label: __("Warehouse"),
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Warehouse",
|
options: "Warehouse",
|
||||||
get_query: function () {
|
get_query: function () {
|
||||||
|
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||||
let company = frappe.query_report.get_filter_value("company");
|
let company = frappe.query_report.get_filter_value("company");
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
company: company,
|
...(warehouse_type && { warehouse_type }),
|
||||||
|
...(company && { company }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ def execute(filters=None):
|
|||||||
|
|
||||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||||
|
|
||||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
if (
|
||||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
sle_count > SLE_COUNT_LIMIT
|
||||||
|
and not filters.get("item_code")
|
||||||
|
and not filters.get("warehouse")
|
||||||
|
and not filters.get("warehouse_type")
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.")
|
||||||
|
)
|
||||||
|
|
||||||
if filters.from_date > filters.to_date:
|
if filters.from_date > filters.to_date:
|
||||||
frappe.throw(_("From Date must be before To Date"))
|
frappe.throw(_("From Date must be before To Date"))
|
||||||
@@ -121,6 +128,16 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
|||||||
)
|
)
|
||||||
|
|
||||||
query = apply_warehouse_filter(query, sle, filters)
|
query = apply_warehouse_filter(query, sle, filters)
|
||||||
|
if filters.warehouse_type and not filters.warehouse:
|
||||||
|
warehouses = frappe.get_all(
|
||||||
|
"Warehouse",
|
||||||
|
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if warehouses:
|
||||||
|
query = query.where(sle.warehouse.isin(warehouses))
|
||||||
|
|
||||||
for field in ["item_code", "batch_no", "company"]:
|
for field in ["item_code", "batch_no", "company"]:
|
||||||
if filters.get(field):
|
if filters.get(field):
|
||||||
query = query.where(sle[field] == filters.get(field))
|
query = query.where(sle[field] == filters.get(field))
|
||||||
@@ -154,6 +171,16 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
|||||||
)
|
)
|
||||||
|
|
||||||
query = apply_warehouse_filter(query, sle, filters)
|
query = apply_warehouse_filter(query, sle, filters)
|
||||||
|
if filters.warehouse_type and not filters.warehouse:
|
||||||
|
warehouses = frappe.get_all(
|
||||||
|
"Warehouse",
|
||||||
|
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if warehouses:
|
||||||
|
query = query.where(sle.warehouse.isin(warehouses))
|
||||||
|
|
||||||
for field in ["item_code", "batch_no", "company"]:
|
for field in ["item_code", "batch_no", "company"]:
|
||||||
if filters.get(field):
|
if filters.get(field):
|
||||||
if field == "batch_no":
|
if field == "batch_no":
|
||||||
|
|||||||
@@ -18,15 +18,24 @@ frappe.query_reports["Stock Ageing"] = {
|
|||||||
default: frappe.datetime.get_today(),
|
default: frappe.datetime.get_today(),
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "warehouse_type",
|
||||||
|
label: __("Warehouse Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
width: "80",
|
||||||
|
options: "Warehouse Type",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldname: "warehouse",
|
fieldname: "warehouse",
|
||||||
label: __("Warehouse"),
|
label: __("Warehouse"),
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Warehouse",
|
options: "Warehouse",
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
const company = frappe.query_report.get_filter_value("company");
|
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||||
|
let company = frappe.query_report.get_filter_value("company");
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
|
...(warehouse_type && { warehouse_type }),
|
||||||
...(company && { company }),
|
...(company && { company }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -434,6 +434,15 @@ class FIFOSlots:
|
|||||||
|
|
||||||
if self.filters.get("warehouse"):
|
if self.filters.get("warehouse"):
|
||||||
sle_query = self.__get_warehouse_conditions(sle, sle_query)
|
sle_query = self.__get_warehouse_conditions(sle, sle_query)
|
||||||
|
elif self.filters.get("warehouse_type"):
|
||||||
|
warehouses = frappe.get_all(
|
||||||
|
"Warehouse",
|
||||||
|
filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if warehouses:
|
||||||
|
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
||||||
|
|
||||||
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
|
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ class StockBalanceReport:
|
|||||||
if self.filters.get("show_stock_ageing_data"):
|
if self.filters.get("show_stock_ageing_data"):
|
||||||
self.sle_entries = self.sle_query.run(as_dict=True)
|
self.sle_entries = self.sle_query.run(as_dict=True)
|
||||||
|
|
||||||
|
# HACK: This is required to avoid causing db query in flt
|
||||||
|
_system_settings = frappe.get_cached_doc("System Settings")
|
||||||
with frappe.db.unbuffered_cursor():
|
with frappe.db.unbuffered_cursor():
|
||||||
if not self.filters.get("show_stock_ageing_data"):
|
if not self.filters.get("show_stock_ageing_data"):
|
||||||
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
|
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
|
||||||
|
|||||||
@@ -231,13 +231,6 @@ def get_columns(filters):
|
|||||||
"width": 100,
|
"width": 100,
|
||||||
"convertible": "qty",
|
"convertible": "qty",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": _("Voucher #"),
|
|
||||||
"fieldname": "voucher_no",
|
|
||||||
"fieldtype": "Dynamic Link",
|
|
||||||
"options": "voucher_type",
|
|
||||||
"width": 150,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": _("Warehouse"),
|
"label": _("Warehouse"),
|
||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
|
|||||||
@@ -56,13 +56,14 @@ def execute(filters=None):
|
|||||||
item_value.setdefault((item, item_map[item]["item_group"]), [])
|
item_value.setdefault((item, item_map[item]["item_group"]), [])
|
||||||
item_value[(item, item_map[item]["item_group"])].append(total_stock_value)
|
item_value[(item, item_map[item]["item_group"])].append(total_stock_value)
|
||||||
|
|
||||||
|
itemwise_brand = frappe._dict(get_itemwise_brand(items))
|
||||||
# sum bal_qty by item
|
# sum bal_qty by item
|
||||||
for (item, item_group), wh_balance in item_balance.items():
|
for (item, item_group), wh_balance in item_balance.items():
|
||||||
if not item_ageing.get(item):
|
if not item_ageing.get(item):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
total_stock_value = sum(item_value[(item, item_group)])
|
total_stock_value = sum(item_value[(item, item_group)])
|
||||||
row = [item, item_map[item]["item_name"], item_group, total_stock_value]
|
row = [item, item_map[item]["item_name"], item_group, itemwise_brand.get(item), total_stock_value]
|
||||||
|
|
||||||
fifo_queue = item_ageing[item]["fifo_queue"]
|
fifo_queue = item_ageing[item]["fifo_queue"]
|
||||||
average_age = 0.00
|
average_age = 0.00
|
||||||
@@ -85,6 +86,10 @@ def execute(filters=None):
|
|||||||
return columns, data
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def get_itemwise_brand(items):
|
||||||
|
return frappe.get_all("Item", filters={"name": ("in", items)}, fields=["name", "brand"], as_list=1)
|
||||||
|
|
||||||
|
|
||||||
def get_columns(filters):
|
def get_columns(filters):
|
||||||
"""return columns"""
|
"""return columns"""
|
||||||
|
|
||||||
@@ -92,6 +97,7 @@ def get_columns(filters):
|
|||||||
_("Item") + ":Link/Item:150",
|
_("Item") + ":Link/Item:150",
|
||||||
_("Item Name") + ":Link/Item:150",
|
_("Item Name") + ":Link/Item:150",
|
||||||
_("Item Group") + "::120",
|
_("Item Group") + "::120",
|
||||||
|
_("Brand") + ":Link/Brand:120",
|
||||||
_("Value") + ":Currency:120",
|
_("Value") + ":Currency:120",
|
||||||
_("Age") + ":Float:120",
|
_("Age") + ":Float:120",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
self.set_stock_value_difference()
|
self.set_stock_value_difference()
|
||||||
|
|
||||||
def get_batch_no_ledgers(self) -> list[dict]:
|
def get_batch_no_ledgers(self) -> list[dict]:
|
||||||
if not self.batchwise_valuation_batches:
|
if not self.batches:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||||
@@ -575,7 +575,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
Sum(child.qty).as_("qty"),
|
Sum(child.qty).as_("qty"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(child.batch_no.isin(self.batchwise_valuation_batches))
|
(child.batch_no.isin(self.batches))
|
||||||
& (parent.warehouse == self.sle.warehouse)
|
& (parent.warehouse == self.sle.warehouse)
|
||||||
& (parent.item_code == self.sle.item_code)
|
& (parent.item_code == self.sle.item_code)
|
||||||
& (parent.docstatus == 1)
|
& (parent.docstatus == 1)
|
||||||
|
|||||||
@@ -67,3 +67,9 @@ typing-modules = ["frappe.types.DF"]
|
|||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
indent-style = "tab"
|
indent-style = "tab"
|
||||||
docstring-code-format = true
|
docstring-code-format = true
|
||||||
|
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://erpnext.com/"
|
||||||
|
Repository = "https://github.com/frappe/erpnext.git"
|
||||||
|
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
||||||
|
|||||||
Reference in New Issue
Block a user