Merge branch 'develop' of https://github.com/frappe/erpnext into immutable_ledger

This commit is contained in:
Deepesh Garg
2024-05-13 17:22:27 +05:30
91 changed files with 17618 additions and 14612 deletions

View File

@@ -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:

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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",

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 = (

View File

@@ -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();
}, },

View File

@@ -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)

View File

@@ -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"]

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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",

View File

@@ -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

View File

@@ -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]

View File

@@ -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",

View File

@@ -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:

View File

@@ -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",

View File

@@ -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 (

View File

@@ -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")

View File

@@ -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",

View File

@@ -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}

View File

@@ -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(

View File

@@ -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)

View File

@@ -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(

View File

@@ -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:

View File

@@ -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":

View File

@@ -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"),

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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",
{ {

View File

@@ -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()

View File

@@ -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",

View File

@@ -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"
): ):

View File

@@ -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:

View File

@@ -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 = {}

View File

@@ -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",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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;
},
}; };

View File

@@ -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:

View File

@@ -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

View File

@@ -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
"""
)

View File

@@ -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);
}, },
}); });
}, },

View File

@@ -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(

View File

@@ -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",

View File

@@ -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;
} }
} }

View File

@@ -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)

View File

@@ -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",

View File

@@ -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",

View File

@@ -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", []);

View File

@@ -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

View File

@@ -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,
}, },
}); });

View File

@@ -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):

View File

@@ -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");
}, },

View File

@@ -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",

View File

@@ -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]

View File

@@ -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 = [
{ {

View File

@@ -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",

View File

@@ -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

View File

@@ -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(

View File

@@ -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)

View File

@@ -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",

View File

@@ -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)
) )

View File

@@ -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))

View File

@@ -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}]

View File

@@ -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)),

View File

@@ -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",

View File

@@ -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:

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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,
) )

View File

@@ -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 }),
}, },
}; };
}, },

View File

@@ -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":

View File

@@ -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 }),
}, },
}; };

View File

@@ -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)

View File

@@ -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)

View File

@@ -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",

View File

@@ -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",
] ]

View File

@@ -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)

View File

@@ -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"