mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-19 15:02:12 +00:00
Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-41384
This commit is contained in:
@@ -59,12 +59,14 @@ repos:
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
|
||||
// hide fields if group
|
||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||
|
||||
// disable fields
|
||||
frm.toggle_enable(["is_group", "company"], false);
|
||||
frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
|
||||
|
||||
if (cint(frm.doc.is_group) == 0) {
|
||||
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
||||
|
||||
@@ -55,8 +55,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"read_only": 1
|
||||
"label": "Account Number"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -72,7 +71,6 @@
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
|
||||
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
|
||||
"General Ledger",
|
||||
"Balance Sheet",
|
||||
"Profit and Loss Statement",
|
||||
"Cash Flow Statement",
|
||||
"Cash Flow",
|
||||
"Accounts Payable",
|
||||
"Accounts Receivable",
|
||||
]) {
|
||||
|
||||
@@ -1525,7 +1525,8 @@
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
"Clients cr\u00e9diteurs": {
|
||||
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
||||
"account_number": "4191"
|
||||
"account_number": "4191",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
||||
"account_number": "4196"
|
||||
@@ -3141,4 +3142,4 @@
|
||||
"account_number": "7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,23 @@
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
enable_immutable_ledger: function (frm) {
|
||||
if (!frm.doc.enable_immutable_ledger) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = __("Enabling this will change the way how cancelled transactions are handled.");
|
||||
msg += " ";
|
||||
msg += __("Please enable only if the understand the effects of enabling this.");
|
||||
msg += "<br>";
|
||||
msg += "Do you still want to enable immutable ledger?";
|
||||
|
||||
frappe.confirm(
|
||||
msg,
|
||||
() => {},
|
||||
() => {
|
||||
frm.set_value("enable_immutable_ledger", 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"column_break_13",
|
||||
"delete_linked_ledger_entries",
|
||||
"enable_immutable_ledger",
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
@@ -105,7 +106,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Supplier Invoice Number Uniqueness"
|
||||
@@ -454,6 +455,13 @@
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Remarks Column Length"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well",
|
||||
"fieldname": "enable_immutable_ledger",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Immutable Ledger"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -461,7 +469,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 14:04:26.553554",
|
||||
"modified": "2024-03-15 12:11:36.085158",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -490,4 +498,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class AccountsSettings(Document):
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
|
||||
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
);
|
||||
|
||||
frm.add_custom_button(__("Auto Reconcile"), function () {
|
||||
if (!frm.doc.bank_account) {
|
||||
frappe.msgprint(__("Please select Bank Account"));
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||
args: {
|
||||
|
||||
@@ -495,12 +495,12 @@ def check_matching(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
|
||||
@@ -540,14 +540,14 @@ def get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
exact_match=None,
|
||||
common_filters=None,
|
||||
):
|
||||
# get queries to get matching vouchers
|
||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||
@@ -580,15 +580,15 @@ def get_matching_queries(
|
||||
bank_account,
|
||||
company,
|
||||
transaction,
|
||||
document_types,
|
||||
exact_match,
|
||||
account_from_to,
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
document_types=None,
|
||||
exact_match=None,
|
||||
account_from_to=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
common_filters=None,
|
||||
):
|
||||
queries = []
|
||||
currency = get_account_currency(bank_account)
|
||||
|
||||
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
|
||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
||||
)
|
||||
|
||||
if "bank_ac_no" in or_filters:
|
||||
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
|
||||
|
||||
if party_result:
|
||||
result = (
|
||||
party,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, format_date, getdate
|
||||
from frappe.utils import add_days, flt, format_date, getdate
|
||||
|
||||
|
||||
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
|
||||
self.validate_child_cost_centers()
|
||||
|
||||
def validate_total_allocation_percentage(self):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
|
||||
@@ -141,7 +141,19 @@ class Dunning(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
self.ignore_linked_doctypes = [
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"Repost Payment Ledger",
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 12:35:38.645968",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -127,6 +127,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -454,7 +454,7 @@ class JournalEntry(AccountsController):
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
@@ -1031,6 +1031,17 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.multi_currency:
|
||||
for row in self.get("accounts"):
|
||||
if row.account_currency != company_currency:
|
||||
self.currency = row.account_currency
|
||||
self.conversion_rate = row.exchange_rate
|
||||
break
|
||||
else:
|
||||
self.currency = company_currency
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"party",
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -88,6 +89,7 @@
|
||||
"custom_remarks",
|
||||
"remarks",
|
||||
"base_in_words",
|
||||
"is_opening",
|
||||
"column_break_16",
|
||||
"letter_head",
|
||||
"print_heading",
|
||||
@@ -750,6 +752,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -765,6 +768,26 @@
|
||||
"label": "In Words",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "company.reconcile_on_advance_payment_date",
|
||||
"fieldname": "reconcile_on_advance_payment_date",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Reconcile on Advance Payment Date",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "No",
|
||||
"depends_on": "eval: doc.book_advance_payments_in_separate_party_account == 1",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Opening",
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -778,7 +801,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-04-11 11:25:07.366347",
|
||||
"modified": "2024-05-31 17:07:06.197249",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -116,11 +116,13 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.book_advance_payments_in_separate_party_account = False
|
||||
if self.party_type not in ("Customer", "Supplier"):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
||||
):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
# Important to set this flag for the gl building logic to work properly
|
||||
@@ -132,6 +134,7 @@ class PaymentEntry(AccountsController):
|
||||
if (account_type == "Payable" and self.party_type == "Customer") or (
|
||||
account_type == "Receivable" and self.party_type == "Supplier"
|
||||
):
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
if self.references:
|
||||
@@ -141,6 +144,7 @@ class PaymentEntry(AccountsController):
|
||||
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
|
||||
if reference_types - allowed_types:
|
||||
self.book_advance_payments_in_separate_party_account = False
|
||||
self.is_opening = "No"
|
||||
return
|
||||
|
||||
liability_account = get_party_account(
|
||||
@@ -1249,13 +1253,16 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
if self.reconcile_on_advance_payment_date:
|
||||
posting_date = self.posting_date
|
||||
else:
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
|
||||
@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
def test_opening_flag_for_advance_as_liability(self):
|
||||
company = "_Test Company"
|
||||
|
||||
advance_account = create_account(
|
||||
parent_account="Current Assets - _TC",
|
||||
account_name="Advances Received",
|
||||
company=company,
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# Enable Advance in separate party account
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": advance_account,
|
||||
},
|
||||
)
|
||||
# Advance Payment
|
||||
adv = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party="_Test Customer",
|
||||
payment_type="Receive",
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
adv.is_opening = "Yes"
|
||||
adv.save() # use save() to trigger set_liability_account()
|
||||
adv.submit()
|
||||
|
||||
gl_with_opening_set = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": adv.name, "is_opening": "Yes"}
|
||||
)
|
||||
# 'Is Opening' can be 'Yes' for Advances in separate party account
|
||||
self.assertNotEqual(gl_with_opening_set, [])
|
||||
|
||||
# Disable Advance in separate party account
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 0,
|
||||
"default_advance_received_account": None,
|
||||
},
|
||||
)
|
||||
payment = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party="_Test Customer",
|
||||
payment_type="Receive",
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
payment.is_opening = "Yes"
|
||||
payment.save()
|
||||
payment.submit()
|
||||
gl_with_opening_set = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": payment.name, "is_opening": "Yes"}
|
||||
)
|
||||
# 'Is Opening' should always be 'No' for normal advance payments
|
||||
self.assertEqual(gl_with_opening_set, [])
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -1525,6 +1525,55 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_advance_payment_reconciliation_date(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconcile_on_advance_payment_date": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 1500
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.posting_date = add_days(nowdate(), -1)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
pi = self.create_purchase_invoice(qty=10, rate=100)
|
||||
self.assertNotEqual(pe.posting_date, pi.posting_date)
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "is_cancelled": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 4)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name, "delinked": 0, "posting_date": pe.posting_date},
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
|
||||
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
|
||||
if self.coupon_code:
|
||||
|
||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
pos.reload()
|
||||
|
||||
pos_return1 = make_sales_return(pos.name)
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
bundle_id = frappe.get_doc(
|
||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||
)
|
||||
|
||||
bundle_id.remove(bundle_id.entries[1])
|
||||
bundle_id.save()
|
||||
|
||||
bundle_id.load_from_db()
|
||||
|
||||
serial_no = bundle_id.entries[0].serial_no
|
||||
self.assertEqual(serial_no, serial_nos[0])
|
||||
|
||||
pos_return1.insert()
|
||||
pos_return1.submit()
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||
self.assertEqual(serial_no, serial_nos[1])
|
||||
|
||||
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if isinstance(error_message, list):
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -74,15 +74,21 @@
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_13",
|
||||
"threshold_percentage",
|
||||
"priority",
|
||||
"dynamic_condition_tab",
|
||||
"condition",
|
||||
"column_break_66",
|
||||
"section_break_13",
|
||||
"apply_multiple_pricing_rules",
|
||||
"apply_discount_on_rate",
|
||||
"column_break_66",
|
||||
"threshold_percentage",
|
||||
"validate_pricing_rule_section",
|
||||
"validate_applied_rule",
|
||||
"column_break_texp",
|
||||
"rule_description",
|
||||
"priority_section",
|
||||
"has_priority",
|
||||
"column_break_sayg",
|
||||
"priority",
|
||||
"help_section",
|
||||
"pricing_rule_help",
|
||||
"reference_section",
|
||||
@@ -477,7 +483,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Advanced Settings"
|
||||
},
|
||||
{
|
||||
@@ -487,6 +493,7 @@
|
||||
"label": "Threshold for Suggestion (In Percentage)"
|
||||
},
|
||||
{
|
||||
"depends_on": "has_priority",
|
||||
"description": "Higher the number, higher the priority",
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Select",
|
||||
@@ -513,6 +520,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
||||
"description": "If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule",
|
||||
"fieldname": "validate_applied_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Applied Rule"
|
||||
@@ -525,7 +533,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "help_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Help Article",
|
||||
"options": "Simple"
|
||||
},
|
||||
{
|
||||
@@ -603,12 +612,42 @@
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Priority"
|
||||
},
|
||||
{
|
||||
"fieldname": "dynamic_condition_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dynamic Condition"
|
||||
},
|
||||
{
|
||||
"fieldname": "validate_pricing_rule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validate Pricing Rule"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_texp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sayg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this checkbox even if you want to set the zero priority",
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:53:34.887358",
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -27,9 +27,7 @@ class PricingRule(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import (
|
||||
PricingRuleItemCode,
|
||||
)
|
||||
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
|
||||
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
||||
PricingRuleItemGroup,
|
||||
)
|
||||
@@ -67,6 +65,7 @@ class PricingRule(Document):
|
||||
free_item_rate: DF.Currency
|
||||
free_item_uom: DF.Link | None
|
||||
free_qty: DF.Float
|
||||
has_priority: DF.Check
|
||||
is_cumulative: DF.Check
|
||||
is_recursive: DF.Check
|
||||
item_groups: DF.Table[PricingRuleItemGroup]
|
||||
@@ -156,6 +155,12 @@ class PricingRule(Document):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
def validate_mandatory(self):
|
||||
if self.has_priority and not self.priority:
|
||||
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))
|
||||
|
||||
if self.priority and not self.has_priority:
|
||||
self.has_priority = 1
|
||||
|
||||
for apply_on, field in apply_on_dict.items():
|
||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||
|
||||
@@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_pricing_rule_for_transaction_with_condition(self):
|
||||
make_item("PR Transaction Condition")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
make_pricing_rule(
|
||||
selling=1,
|
||||
min_qty=0,
|
||||
price_or_product_discount="Product",
|
||||
apply_on="Transaction",
|
||||
free_item="PR Transaction Condition",
|
||||
free_qty=1,
|
||||
free_item_rate=10,
|
||||
condition="customer=='_Test Customer 1'",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
|
||||
self.assertEqual(len(si.items), 2)
|
||||
self.assertEqual(si.items[1].rate, 10)
|
||||
|
||||
si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
|
||||
self.assertEqual(len(si1.items), 1)
|
||||
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_remove_pricing_rule(self):
|
||||
item = make_item("Water Flask")
|
||||
make_item_price("Water Flask", "_Test Price List", 100)
|
||||
@@ -1157,6 +1181,62 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_priority_of_multiple_pricing_rules(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 1",
|
||||
"name": "_Test Pricing Rule 1",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 10,
|
||||
"has_priority": 1,
|
||||
"priority": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule 2",
|
||||
"name": "_Test Pricing Rule 2",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"price_or_product_discount": "Price",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"has_priority": 1,
|
||||
"priority": 3,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
|
||||
self.assertEqual(so.items[0].discount_percentage, 20)
|
||||
self.assertEqual(so.items[0].rate, 800)
|
||||
|
||||
frappe.delete_doc_if_exists("Sales Order", so.name)
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
@@ -1185,6 +1265,7 @@ def make_pricing_rule(**args):
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ def get_pricing_rules(args, doc=None):
|
||||
|
||||
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||
if pricing_rules and pricing_rules[0].has_priority:
|
||||
continue
|
||||
|
||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||
break
|
||||
|
||||
@@ -561,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
|
||||
if pricing_rules:
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
|
||||
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
|
||||
|
||||
if not pricing_rules:
|
||||
remove_free_item(doc)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -504,7 +506,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if isinstance(for_filter, str):
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
for_filter = json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
|
||||
@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
|
||||
ageing_filters = frappe._dict(
|
||||
{
|
||||
"company": doc.company,
|
||||
"report_date": doc.to_date,
|
||||
"report_date": doc.posting_date,
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
|
||||
@@ -340,10 +340,11 @@
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">30 Days</th>
|
||||
<th style="width: 25%">60 Days</th>
|
||||
<th style="width: 25%">90 Days</th>
|
||||
<th style="width: 25%">120 Days</th>
|
||||
<th style="width: 25%">0 - 30 Days</th>
|
||||
<th style="width: 25%">30 - 60 Days</th>
|
||||
<th style="width: 25%">60 - 90 Days</th>
|
||||
<th style="width: 25%">90 - 120 Days</th>
|
||||
<th style="width: 20%">Above 120 Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -352,6 +353,7 @@
|
||||
<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.range4, currency=data[0]["currency"]) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -485,10 +485,12 @@ function hide_fields(doc) {
|
||||
|
||||
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
||||
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
if (cur_frm.fields_dict["items"]) {
|
||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||
item_fields_stock,
|
||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||
);
|
||||
}
|
||||
|
||||
cur_frm.refresh_fields();
|
||||
}
|
||||
@@ -675,7 +677,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
if (frm.doc.supplier) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||
}
|
||||
if (!frm.doc.__onload.supplier_tds) {
|
||||
if (!frm.doc.__onload.enable_apply_tds) {
|
||||
frm.set_df_property("apply_tds", "read_only", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
@@ -347,6 +347,22 @@ class PurchaseInvoice(BuyingController):
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
|
||||
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
|
||||
po = qb.DocType("Purchase Order")
|
||||
po_with_tds = (
|
||||
qb.from_(po)
|
||||
.select(po.name)
|
||||
.where(
|
||||
po.docstatus.eq(1)
|
||||
& (po.name.isin(purchase_orders))
|
||||
& (po.apply_tds.eq(1))
|
||||
& (po.tax_withholding_category.notnull())
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.set_onload("enable_apply_tds", True if po_with_tds else False)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
@@ -448,7 +464,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_received_but_not_billed = None
|
||||
self.asset_received_but_not_billed = None
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -531,26 +547,45 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif item.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
asset_category_account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account"),
|
||||
account = None
|
||||
if item.pr_detail:
|
||||
if not self.asset_received_but_not_billed:
|
||||
self.asset_received_but_not_billed = self.get_company_default(
|
||||
"asset_received_but_not_billed"
|
||||
)
|
||||
|
||||
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||
arbnb_booked_in_pr = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": item.purchase_receipt,
|
||||
"account": self.asset_received_but_not_billed,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
if arbnb_booked_in_pr:
|
||||
account = self.asset_received_but_not_billed
|
||||
|
||||
if not account:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(
|
||||
form_link, self.company
|
||||
),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
item.expense_account = account
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
|
||||
@@ -648,6 +683,19 @@ class PurchaseInvoice(BuyingController):
|
||||
where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""",
|
||||
}
|
||||
)
|
||||
self.status_updater.append(
|
||||
{
|
||||
"source_dt": "Purchase Invoice Item",
|
||||
"target_dt": "Material Request Item",
|
||||
"join_field": "material_request_item",
|
||||
"target_field": "received_qty",
|
||||
"target_parent_dt": "Material Request",
|
||||
"target_parent_field": "per_received",
|
||||
"target_ref_field": "stock_qty",
|
||||
"source_field": "stock_qty",
|
||||
"percent_join_field": "material_request",
|
||||
}
|
||||
)
|
||||
if cint(self.is_return):
|
||||
self.status_updater.append(
|
||||
{
|
||||
@@ -707,6 +755,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -1035,10 +1084,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
|
||||
provisional_account, pr_qty, pr_base_rate, pr_rate = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
item.pr_detail,
|
||||
["provisional_expense_account", "qty", "base_rate"],
|
||||
["provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
provisional_account = provisional_account or self.get_company_default(
|
||||
"default_provisional_account"
|
||||
@@ -1072,7 +1121,10 @@ class PurchaseInvoice(BuyingController):
|
||||
self.posting_date,
|
||||
provisional_account,
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
|
||||
item_amount=(
|
||||
(min(item.qty, pr_qty) * pr_rate)
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
@@ -1182,7 +1234,7 @@ class PurchaseInvoice(BuyingController):
|
||||
asset.name,
|
||||
{
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||
)
|
||||
@@ -72,6 +74,31 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
# teardown
|
||||
pi.delete()
|
||||
|
||||
def test_update_received_qty_in_material_request(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
|
||||
"""
|
||||
Test if the received_qty in Material Request is updated correctly when
|
||||
a Purchase Invoice with update_stock=True is submitted.
|
||||
"""
|
||||
mr = make_material_request(item_code="_Test Item", qty=10)
|
||||
mr.save()
|
||||
mr.submit()
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
# Create a Purchase Invoice with update_stock=True
|
||||
pi = make_purchase_invoice(po.name)
|
||||
pi.update_stock = True
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
# Check if the received quantity is updated in Material Request
|
||||
mr.reload()
|
||||
self.assertEqual(mr.items[0].received_qty, 10)
|
||||
|
||||
def test_gl_entries_without_perpetual_inventory(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||
pi = frappe.copy_doc(test_records[0])
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"purchase_receipt",
|
||||
"pr_detail",
|
||||
"sales_invoice_item",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"item_weight_details",
|
||||
"weight_per_unit",
|
||||
"total_weight",
|
||||
@@ -934,12 +936,34 @@
|
||||
{
|
||||
"fieldname": "column_break_vbbb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Material Request",
|
||||
"no_copy": 1,
|
||||
"options": "Material Request",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Material Request Item",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "pr_detail",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-19 19:09:47.210965",
|
||||
"modified": "2024-06-14 11:57:07.171700",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@@ -949,4 +973,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ class PurchaseInvoiceItem(Document):
|
||||
manufacturer_part_no: DF.Data | None
|
||||
margin_rate_or_amount: DF.Float
|
||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
net_amount: DF.Currency
|
||||
net_rate: DF.Currency
|
||||
page_break: DF.Check
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -55,14 +53,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:27.362567",
|
||||
"modified": "2024-06-03 17:30:37.012593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -71,7 +70,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-07 14:24:13.321522",
|
||||
"modified": "2024-06-06 13:56:37.908879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
@@ -30,13 +30,17 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1
|
||||
"select": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-10-19 21:59:33.553852",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -99,13 +98,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:35.719727",
|
||||
"modified": "2024-06-03 17:31:04.472279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -114,7 +115,9 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -787,6 +787,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheets",
|
||||
"no_copy": 1,
|
||||
"options": "Sales Invoice Timesheet",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -2187,7 +2188,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:02:28.549041",
|
||||
"modified": "2024-06-07 16:49:32.458402",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -2677,6 +2678,10 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
|
||||
# update outstanding
|
||||
if source.payment_schedule and len(source.payment_schedule) == 1:
|
||||
target.overdue_payments[0].outstanding = source.get("outstanding_amount")
|
||||
|
||||
target.validate()
|
||||
|
||||
return get_mapped_doc(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
@@ -3720,9 +3721,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
|
||||
@@ -870,7 +870,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -926,7 +927,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 15:56:44.828634",
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -936,4 +937,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -161,7 +163,7 @@ def get_linked_payments_for_doc(
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
selections = json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
|
||||
@@ -577,6 +577,8 @@ def make_reverse_gl_entries(
|
||||
and make reverse gl entries by swapping debit and credit
|
||||
"""
|
||||
|
||||
immutable_ledger_enabled = is_immutable_ledger_enabled()
|
||||
|
||||
if not gl_entries:
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
@@ -608,7 +610,6 @@ def make_reverse_gl_entries(
|
||||
for x in gl_entries:
|
||||
query = (
|
||||
frappe.qb.update(gle)
|
||||
.set(gle.is_cancelled, True)
|
||||
.set(gle.modified, now())
|
||||
.set(gle.modified_by, frappe.session.user)
|
||||
.where(
|
||||
@@ -623,9 +624,14 @@ def make_reverse_gl_entries(
|
||||
& (gle.voucher_detail_no == x.voucher_detail_no)
|
||||
)
|
||||
)
|
||||
|
||||
if not immutable_ledger_enabled:
|
||||
query = query.set(gle.is_cancelled, True)
|
||||
|
||||
query.run()
|
||||
else:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
@@ -644,6 +650,10 @@ def make_reverse_gl_entries(
|
||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||
new_gle["is_cancelled"] = 1
|
||||
|
||||
if immutable_ledger_enabled:
|
||||
new_gle["is_cancelled"] = 0
|
||||
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
|
||||
|
||||
if new_gle["debit"] or new_gle["credit"]:
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
@@ -736,3 +746,7 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
|
||||
def is_immutable_ledger_enabled():
|
||||
return frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||
|
||||
@@ -49,7 +49,6 @@ def get_conditions(filters):
|
||||
|
||||
if filters.account_type:
|
||||
conditions["account_type"] = filters.account_type
|
||||
return conditions
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
@@ -1028,20 +1028,6 @@ class ReceivablePayableReport:
|
||||
fieldtype="Link",
|
||||
options="Contact",
|
||||
)
|
||||
if self.filters.party_type == "Customer":
|
||||
self.add_column(
|
||||
_("Customer Name"),
|
||||
fieldname="customer_name",
|
||||
fieldtype="Link",
|
||||
options="Customer",
|
||||
)
|
||||
elif self.filters.party_type == "Supplier":
|
||||
self.add_column(
|
||||
_("Supplier Name"),
|
||||
fieldname="supplier_name",
|
||||
fieldtype="Link",
|
||||
options="Supplier",
|
||||
)
|
||||
|
||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
|
||||
@@ -75,11 +76,24 @@ def get_data(filters):
|
||||
asset_data = assets_details.get(d.against_voucher)
|
||||
if asset_data:
|
||||
if not asset_data.get("accumulated_depreciation_amount"):
|
||||
asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
|
||||
"opening_accumulated_depreciation"
|
||||
)
|
||||
AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
|
||||
DepreciationSchedule = DocType("Depreciation Schedule")
|
||||
query = (
|
||||
frappe.qb.from_(DepreciationSchedule)
|
||||
.join(AssetDepreciationSchedule)
|
||||
.on(DepreciationSchedule.parent == AssetDepreciationSchedule.name)
|
||||
.select(DepreciationSchedule.accumulated_depreciation_amount)
|
||||
.where(
|
||||
(AssetDepreciationSchedule.asset == d.against_voucher)
|
||||
& (DepreciationSchedule.parenttype == "Asset Depreciation Schedule")
|
||||
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||
|
||||
else:
|
||||
asset_data.accumulated_depreciation_amount += d.debit
|
||||
asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit
|
||||
|
||||
row = frappe._dict(asset_data)
|
||||
row.update(
|
||||
|
||||
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition += " and asset_category = %(asset_category)s"
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT asset_category,
|
||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_as_on_from_date,
|
||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_new_purchase,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Sold" then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Sold" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_sold_asset,
|
||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
||||
and disposal_date >= %(from_date)s
|
||||
and disposal_date <= %(to_date)s then
|
||||
case when status = "Scrapped" then
|
||||
gross_purchase_amount
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||
and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s then
|
||||
case when a.status = "Scrapped" then
|
||||
a.gross_purchase_amount
|
||||
else
|
||||
0
|
||||
end
|
||||
else
|
||||
0
|
||||
end), 0) as cost_of_scrapped_asset
|
||||
from `tabAsset`
|
||||
from `tabAsset` a
|
||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
|
||||
group by asset_category
|
||||
and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
|
||||
group by a.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
|
||||
@@ -219,7 +219,8 @@ def get_conditions(filters):
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
if filters.account:
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@@ -329,7 +330,7 @@ def get_accounts_with_children(accounts):
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
return list(set(all_accounts)) if all_accounts else None
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
@@ -420,6 +421,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
if filters.get("show_net_values_in_party_account"):
|
||||
account_type_map = get_account_type_map(filters.get("company"))
|
||||
|
||||
immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||
|
||||
def update_value_in_dict(data, key, gle):
|
||||
data[key].debit += gle.debit
|
||||
data[key].credit += gle.credit
|
||||
@@ -481,12 +484,17 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
elif group_by_voucher_consolidated:
|
||||
keylist = [
|
||||
gle.get("posting_date"),
|
||||
gle.get("voucher_type"),
|
||||
gle.get("voucher_no"),
|
||||
gle.get("account"),
|
||||
gle.get("party_type"),
|
||||
gle.get("party"),
|
||||
]
|
||||
|
||||
if immutable_ledger:
|
||||
keylist.append(gle.get("creation"))
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
add_sub_total_row,
|
||||
add_total_row,
|
||||
apply_group_by_conditions,
|
||||
get_grand_total,
|
||||
get_group_by_and_display_fields,
|
||||
get_group_by_conditions,
|
||||
get_tax_accounts,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
@@ -29,7 +30,7 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
|
||||
item_list = get_items(filters, get_query_columns(additional_table_columns))
|
||||
item_list = get_items(filters, additional_table_columns)
|
||||
aii_account_map = get_aii_accounts()
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(
|
||||
@@ -287,59 +288,90 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
def apply_conditions(query, pi, pii, filters):
|
||||
for opts in ("company", "supplier", "mode_of_payment"):
|
||||
if filters.get(opts):
|
||||
query = query.where(pi[opts] == filters[opts])
|
||||
|
||||
for opts in (
|
||||
("company", " and `tabPurchase Invoice`.company=%(company)s"),
|
||||
("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
|
||||
("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
|
||||
("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
|
||||
("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
|
||||
("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
|
||||
("item_group", " and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s"),
|
||||
):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
if filters.get("from_date"):
|
||||
query = query.where(pi.posting_date >= filters.get("from_date"))
|
||||
|
||||
if filters.get("to_date"):
|
||||
query = query.where(pi.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(pii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
query = query.where(pii.item_group == filters.get("item_group"))
|
||||
|
||||
if not filters.get("group_by"):
|
||||
conditions += (
|
||||
"ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc"
|
||||
)
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
conditions += get_group_by_conditions(filters, "Purchase Invoice")
|
||||
query = apply_group_by_conditions(filters, "Purchase Invoice")
|
||||
|
||||
return conditions
|
||||
return query
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns):
|
||||
conditions = get_conditions(filters)
|
||||
if additional_query_columns:
|
||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
|
||||
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
|
||||
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
|
||||
`tabPurchase Invoice`.unrealized_profit_loss_account,
|
||||
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
|
||||
`tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
|
||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
||||
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
|
||||
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
|
||||
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
|
||||
`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
|
||||
`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {additional_query_columns}
|
||||
from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
|
||||
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
|
||||
`tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
|
||||
`tabPurchase Invoice`.docstatus = 1 {conditions}
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
def get_items(filters, additional_table_columns):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
.join(pii)
|
||||
.on(pi.name == pii.parent)
|
||||
.left_join(Item)
|
||||
.on(pii.item_code == Item.name)
|
||||
.select(
|
||||
pii.name,
|
||||
pii.parent,
|
||||
pi.posting_date,
|
||||
pi.credit_to,
|
||||
pi.company,
|
||||
pi.supplier,
|
||||
pi.remarks,
|
||||
pi.base_net_total,
|
||||
pi.unrealized_profit_loss_account,
|
||||
pii.item_code,
|
||||
pii.description,
|
||||
pii.item_group,
|
||||
pii.item_name.as_("pi_item_name"),
|
||||
pii.item_group.as_("pi_item_group"),
|
||||
Item.item_name.as_("i_item_name"),
|
||||
Item.item_group.as_("i_item_group"),
|
||||
pii.project,
|
||||
pii.purchase_order,
|
||||
pii.purchase_receipt,
|
||||
pii.po_detail,
|
||||
pii.expense_account,
|
||||
pii.stock_qty,
|
||||
pii.stock_uom,
|
||||
pii.base_net_amount,
|
||||
pi.supplier_name,
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
)
|
||||
|
||||
if filters.get("supplier"):
|
||||
query = query.where(pi.supplier == filters["supplier"])
|
||||
if filters.get("company"):
|
||||
query = query.where(pi.company == filters["company"])
|
||||
|
||||
if additional_table_columns:
|
||||
for column in additional_table_columns:
|
||||
if column.get("_doctype"):
|
||||
table = frappe.qb.DocType(column.get("_doctype"))
|
||||
query = query.select(table[column.get("fieldname")])
|
||||
else:
|
||||
query = query.select(pi[column.get("fieldname")])
|
||||
|
||||
query = apply_conditions(query, pi, pii, filters)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_aii_accounts():
|
||||
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
||||
|
||||
@@ -41,6 +41,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
get_query: function () {
|
||||
const company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: { company: company },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "brand",
|
||||
@@ -48,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Brand",
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
@@ -26,7 +27,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, get_query_columns(additional_table_columns), additional_conditions)
|
||||
item_list = get_items(filters, additional_table_columns, additional_conditions)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
@@ -83,9 +84,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
"company": d.company,
|
||||
"sales_order": d.sales_order,
|
||||
"delivery_note": d.delivery_note,
|
||||
"income_account": d.unrealized_profit_loss_account
|
||||
if d.is_internal_customer == 1
|
||||
else d.income_account,
|
||||
"income_account": get_income_account(d),
|
||||
"cost_center": d.cost_center,
|
||||
"stock_qty": d.stock_qty,
|
||||
"stock_uom": d.stock_uom,
|
||||
@@ -150,6 +149,15 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
return columns, data, None, None, None, skip_total_row
|
||||
|
||||
|
||||
def get_income_account(row):
|
||||
if row.enable_deferred_revenue:
|
||||
return row.deferred_revenue_account
|
||||
elif row.is_internal_customer == 1:
|
||||
return row.unrealized_profit_loss_account
|
||||
else:
|
||||
return row.income_account
|
||||
|
||||
|
||||
def get_columns(additional_table_columns, filters):
|
||||
columns = []
|
||||
|
||||
@@ -333,93 +341,143 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters, additional_conditions=None):
|
||||
conditions = ""
|
||||
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
for opts in ("company", "customer"):
|
||||
if filters.get(opts):
|
||||
query = query.where(si[opts] == filters[opts])
|
||||
|
||||
for opts in (
|
||||
("company", " and `tabSales Invoice`.company=%(company)s"),
|
||||
("customer", " and `tabSales Invoice`.customer = %(customer)s"),
|
||||
("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"),
|
||||
("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"),
|
||||
("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"),
|
||||
):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
if filters.get("from_date"):
|
||||
query = query.where(si.posting_date >= filters.get("from_date"))
|
||||
|
||||
if additional_conditions:
|
||||
conditions += additional_conditions
|
||||
if filters.get("to_date"):
|
||||
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
||||
sales_invoice = frappe.db.get_all(
|
||||
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
|
||||
)
|
||||
query = query.where(si.name.isin(sales_invoice))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||
lft, rgt = frappe.db.get_all(
|
||||
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
||||
)[0]
|
||||
conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
|
||||
warehouses = frappe.db.get_all("Warehouse", {"lft": (">", lft), "rgt": ("<", rgt)}, pluck="name")
|
||||
query = query.where(sii.warehouse.isin(warehouses))
|
||||
else:
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
|
||||
query = query.where(sii.warehouse == filters.get("warehouse"))
|
||||
|
||||
if filters.get("brand"):
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""
|
||||
query = query.where(sii.brand == filters.get("brand"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(sii.item_code == filters.get("item_code"))
|
||||
|
||||
if filters.get("item_group"):
|
||||
conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s"""
|
||||
query = query.where(sii.item_group == filters.get("item_group"))
|
||||
|
||||
if filters.get("income_account"):
|
||||
query = query.where(
|
||||
(sii.income_account == filters.get("income_account"))
|
||||
| (sii.deferred_revenue_account == filters.get("income_account"))
|
||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||
)
|
||||
|
||||
if not filters.get("group_by"):
|
||||
conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc"
|
||||
query = query.orderby(si.posting_date, order=Order.desc)
|
||||
query = query.orderby(sii.item_group, order=Order.desc)
|
||||
else:
|
||||
conditions += get_group_by_conditions(filters, "Sales Invoice")
|
||||
query = apply_group_by_conditions(query, si, sii, filters)
|
||||
|
||||
return conditions
|
||||
for key, value in (additional_conditions or {}).items():
|
||||
query = query.where(si[key] == value)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_group_by_conditions(filters, doctype):
|
||||
def apply_group_by_conditions(query, si, ii, filters):
|
||||
if filters.get("group_by") == "Invoice":
|
||||
return f"ORDER BY `tab{doctype} Item`.parent desc"
|
||||
query = query.orderby(ii.parent, order=Order.desc)
|
||||
elif filters.get("group_by") == "Item":
|
||||
return f"ORDER BY `tab{doctype} Item`.`item_code`"
|
||||
query = query.orderby(ii.item_code)
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
return "ORDER BY `tab{} Item`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
query = query.orderby(ii.item_group)
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
return "ORDER BY `tab{}`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
conditions = get_conditions(filters, additional_conditions)
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(si)
|
||||
.join(sii)
|
||||
.on(si.name == sii.parent)
|
||||
.left_join(item)
|
||||
.on(sii.item_code == item.name)
|
||||
.select(
|
||||
sii.name,
|
||||
sii.parent,
|
||||
si.posting_date,
|
||||
si.debit_to,
|
||||
si.unrealized_profit_loss_account,
|
||||
si.is_internal_customer,
|
||||
si.customer,
|
||||
si.remarks,
|
||||
si.territory,
|
||||
si.company,
|
||||
si.base_net_total,
|
||||
sii.project,
|
||||
sii.item_code,
|
||||
sii.description,
|
||||
sii.item_name,
|
||||
sii.item_group,
|
||||
sii.item_name.as_("si_item_name"),
|
||||
sii.item_group.as_("si_item_group"),
|
||||
item.item_name.as_("i_item_name"),
|
||||
item.item_group.as_("i_item_group"),
|
||||
sii.sales_order,
|
||||
sii.delivery_note,
|
||||
sii.income_account,
|
||||
sii.cost_center,
|
||||
sii.enable_deferred_revenue,
|
||||
sii.deferred_revenue_account,
|
||||
sii.stock_qty,
|
||||
sii.stock_uom,
|
||||
sii.base_net_rate,
|
||||
sii.base_net_amount,
|
||||
si.customer_name,
|
||||
si.customer_group,
|
||||
sii.so_detail,
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
sii.qty,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
|
||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
|
||||
`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
|
||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
||||
`tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
|
||||
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {}
|
||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
|
||||
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
|
||||
`tabItem`.name = `tabSales Invoice Item`.`item_code` and
|
||||
`tabSales Invoice`.docstatus = 1 {}
|
||||
""".format(additional_query_columns, conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
for column in additional_query_columns:
|
||||
if column.get("_doctype"):
|
||||
table = frappe.qb.DocType(column.get("_doctype"))
|
||||
query = query.select(table[column.get("fieldname")])
|
||||
else:
|
||||
query = query.select(si[column.get("fieldname")])
|
||||
|
||||
if filters.get("customer"):
|
||||
query = query.where(si.customer == filters["customer"])
|
||||
|
||||
if filters.get("customer_group"):
|
||||
query = query.where(si.customer_group == filters["customer_group"])
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_delivery_notes_against_sales_order(item_list):
|
||||
@@ -427,16 +485,14 @@ def get_delivery_notes_against_sales_order(item_list):
|
||||
so_item_rows = list(set([d.so_detail for d in item_list]))
|
||||
|
||||
if so_item_rows:
|
||||
delivery_notes = frappe.db.sql(
|
||||
"""
|
||||
select parent, so_detail
|
||||
from `tabDelivery Note Item`
|
||||
where docstatus=1 and so_detail in (%s)
|
||||
group by so_detail, parent
|
||||
"""
|
||||
% (", ".join(["%s"] * len(so_item_rows))),
|
||||
tuple(so_item_rows),
|
||||
as_dict=1,
|
||||
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||
delivery_notes = (
|
||||
frappe.qb.from_(dn_item)
|
||||
.select(dn_item.parent, dn_item.so_detail)
|
||||
.where(dn_item.docstatus == 1)
|
||||
.where(dn_item.so_detail.isin(so_item_rows))
|
||||
.groupby(dn_item.so_detail, dn_item.parent)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for dn in delivery_notes:
|
||||
@@ -446,15 +502,16 @@ def get_delivery_notes_against_sales_order(item_list):
|
||||
|
||||
|
||||
def get_grand_total(filters, doctype):
|
||||
return frappe.db.sql(
|
||||
f""" SELECT
|
||||
SUM(`tab{doctype}`.base_grand_total)
|
||||
FROM `tab{doctype}`
|
||||
WHERE `tab{doctype}`.docstatus = 1
|
||||
and posting_date between %s and %s
|
||||
""",
|
||||
(filters.get("from_date"), filters.get("to_date")),
|
||||
)[0][0] # nosec
|
||||
return flt(
|
||||
frappe.db.get_value(
|
||||
doctype,
|
||||
{
|
||||
"docstatus": 1,
|
||||
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||
},
|
||||
"sum(base_grand_total)",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_tax_accounts(
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for d in entries:
|
||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
||||
|
||||
if d.reference_type == "Purchase Invoice":
|
||||
payment_amount = flt(d.debit) or -1 * flt(d.credit)
|
||||
else:
|
||||
payment_amount = flt(d.credit) or -1 * flt(d.debit)
|
||||
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||
payment_amount = d.amount
|
||||
|
||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
||||
|
||||
if d.against_voucher:
|
||||
if d.against_voucher_no:
|
||||
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||
|
||||
row = [
|
||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
||||
d.party_type,
|
||||
d.party,
|
||||
d.posting_date,
|
||||
d.against_voucher,
|
||||
d.against_voucher_no,
|
||||
invoice.posting_date,
|
||||
invoice.due_date,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.amount,
|
||||
d.remarks,
|
||||
d.age,
|
||||
d.range1,
|
||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
},
|
||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = []
|
||||
|
||||
if not filters.party_type:
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
filters.party_type = "Supplier"
|
||||
else:
|
||||
filters.party_type = "Customer"
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("party_type=%(party_type)s")
|
||||
conditions.append(ple.delinked.eq(0))
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
conditions.append(ple.party_type.eq("Supplier"))
|
||||
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||
else:
|
||||
conditions.append(ple.party_type.eq("Customer"))
|
||||
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||
|
||||
if filters.party:
|
||||
conditions.append("party=%(party)s")
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("against_voucher_type=%(reference_type)s")
|
||||
filters["reference_type"] = (
|
||||
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
|
||||
)
|
||||
conditions.append(ple.party.eq(filters.party))
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append("posting_date >= %(from_date)s")
|
||||
conditions.append(ple.posting_date.gte(filters.get("from_date")))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append("posting_date <= %(to_date)s")
|
||||
conditions.append(ple.posting_date.lte(filters.get("to_date")))
|
||||
|
||||
return "and " + " and ".join(conditions) if conditions else ""
|
||||
if filters.get("company"):
|
||||
conditions.append(ple.company.eq(filters.get("company")))
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
||||
""".format(get_conditions(filters)),
|
||||
filters,
|
||||
as_dict=1,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
Abs(ple.amount).as_("amount"),
|
||||
ple.remarks,
|
||||
ple.against_voucher_no,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
|
||||
|
||||
def get_invoice_posting_date_map(filters):
|
||||
invoice_details = {}
|
||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
||||
dt = (
|
||||
qb.DocType("Sales Invoice")
|
||||
if filters.get("payment_type") == _("Incoming")
|
||||
else qb.DocType("Purchase Invoice")
|
||||
)
|
||||
res = (
|
||||
qb.from_(dt)
|
||||
.select(dt.name, dt.posting_date, dt.due_date)
|
||||
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
|
||||
.run(as_dict=1)
|
||||
)
|
||||
for t in res:
|
||||
invoice_details[t.name] = t
|
||||
|
||||
return invoice_details
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
|
||||
frappe.query_reports["Purchase Invoice Trends"] = {
|
||||
filters: erpnext.get_purchase_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Purchase Invoice Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/sales_trends_filters.js", function () {
|
||||
frappe.query_reports["Sales Invoice Trends"] = {
|
||||
filters: erpnext.get_sales_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Sales Invoice Trends"] = $.extend({}, erpnext.sales_trends_filters);
|
||||
|
||||
@@ -80,6 +80,7 @@ def _execute(filters, additional_table_columns=None):
|
||||
delivery_note = list(set(invoice_so_dn_map.get(inv.name, {}).get("delivery_note", [])))
|
||||
cost_center = list(set(invoice_cc_wh_map.get(inv.name, {}).get("cost_center", [])))
|
||||
warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", [])))
|
||||
inv_customer_details = customer_details.get(inv.customer, {})
|
||||
|
||||
row = {
|
||||
"voucher_type": inv.doctype,
|
||||
@@ -88,9 +89,9 @@ def _execute(filters, additional_table_columns=None):
|
||||
"customer": inv.customer,
|
||||
"customer_name": inv.customer_name,
|
||||
**get_values_for_columns(additional_table_columns, inv),
|
||||
"customer_group": customer_details.get(inv.customer).get("customer_group"),
|
||||
"territory": customer_details.get(inv.customer).get("territory"),
|
||||
"tax_id": customer_details.get(inv.customer).get("tax_id"),
|
||||
"customer_group": inv_customer_details.get("customer_group"),
|
||||
"territory": inv_customer_details.get("territory"),
|
||||
"tax_id": inv_customer_details.get("tax_id"),
|
||||
"receivable_account": inv.debit_to,
|
||||
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
|
||||
"project": inv.project,
|
||||
|
||||
@@ -56,7 +56,7 @@ def get_fiscal_year(
|
||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
|
||||
):
|
||||
if isinstance(boolean, str):
|
||||
boolean = frappe.json.loads(boolean)
|
||||
boolean = loads(boolean)
|
||||
|
||||
fiscal_years = get_fiscal_years(
|
||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
|
||||
|
||||
@@ -652,7 +652,7 @@ frappe.ui.form.on("Asset", {
|
||||
);
|
||||
|
||||
frm.set_value("gross_purchase_amount", purchase_amount);
|
||||
frm.set_value("purchase_receipt_amount", purchase_amount);
|
||||
frm.set_value("purchase_amount", purchase_amount);
|
||||
frm.set_value("asset_quantity", asset_quantity);
|
||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||
if (item.asset_location) {
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"status",
|
||||
"booked_fixed_asset",
|
||||
"column_break_51",
|
||||
"purchase_receipt_amount",
|
||||
"purchase_amount",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from",
|
||||
@@ -408,15 +408,6 @@
|
||||
"options": "Purchase Receipt",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Amount",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
||||
"fieldname": "purchase_invoice",
|
||||
@@ -546,6 +537,15 @@
|
||||
"label": "Additional Asset Cost",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Amount",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -589,7 +589,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-01-15 17:35:49.226603",
|
||||
"modified": "2024-04-18 16:45:47.306032",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -633,4 +633,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +92,10 @@ class Asset(AccountsController):
|
||||
number_of_depreciations_booked: DF.Int
|
||||
opening_accumulated_depreciation: DF.Currency
|
||||
policy_number: DF.Data | None
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_amount: DF.Currency
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -354,7 +354,7 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
|
||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
|
||||
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_amount:
|
||||
error_message = _(
|
||||
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
||||
)
|
||||
@@ -696,7 +696,7 @@ class Asset(AccountsController):
|
||||
purchase_document = self.get_purchase_document()
|
||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||
|
||||
if purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate():
|
||||
if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -704,8 +704,8 @@ class Asset(AccountsController):
|
||||
"against": fixed_asset_account,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"posting_date": self.available_for_use_date,
|
||||
"credit": self.purchase_receipt_amount,
|
||||
"credit_in_account_currency": self.purchase_receipt_amount,
|
||||
"credit": self.purchase_amount,
|
||||
"credit_in_account_currency": self.purchase_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
@@ -719,8 +719,8 @@ class Asset(AccountsController):
|
||||
"against": cwip_account,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"posting_date": self.available_for_use_date,
|
||||
"debit": self.purchase_receipt_amount,
|
||||
"debit_in_account_currency": self.purchase_receipt_amount,
|
||||
"debit": self.purchase_amount,
|
||||
"debit_in_account_currency": self.purchase_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
@@ -1116,8 +1116,8 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
)
|
||||
|
||||
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
||||
if asset.purchase_receipt_amount:
|
||||
new_asset.purchase_receipt_amount = new_gross_purchase_amount
|
||||
if asset.purchase_amount:
|
||||
new_asset.purchase_amount = new_gross_purchase_amount
|
||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||
new_asset.asset_quantity = split_qty
|
||||
new_asset.split_from = asset.name
|
||||
@@ -1140,6 +1140,8 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
|
||||
for row in new_asset.get("finance_books"):
|
||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
||||
if not current_asset_depr_schedule_doc:
|
||||
continue
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
|
||||
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
|
||||
|
||||
@@ -1000,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
|
||||
)
|
||||
self.assertEqual(depreciation_amount, 30000)
|
||||
@@ -1698,7 +1698,7 @@ def create_asset(**args):
|
||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
||||
"purchase_receipt_amount": args.purchase_receipt_amount or 100000,
|
||||
"purchase_amount": args.purchase_amount or 100000,
|
||||
"maintenance_required": args.maintenance_required or 0,
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||
@@ -1723,6 +1723,7 @@ def create_asset(**args):
|
||||
"depreciation_start_date": args.depreciation_start_date,
|
||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||
"shift_based": args.shift_based or 0,
|
||||
"rate_of_depreciation": args.rate_of_depreciation or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ class AssetCapitalization(StockController):
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
super(AssetCapitalization, self).on_trash()
|
||||
super().on_trash()
|
||||
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
@@ -616,8 +616,7 @@ class AssetCapitalization(StockController):
|
||||
asset_doc.available_for_use_date = self.posting_date
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||
@@ -653,7 +652,7 @@ class AssetCapitalization(StockController):
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
@@ -526,7 +526,7 @@ def create_depreciation_asset(**args):
|
||||
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||
|
||||
asset.gross_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_receipt_amount = asset.gross_purchase_amount
|
||||
asset.purchase_amount = asset.gross_purchase_amount
|
||||
|
||||
finance_book = asset.append("finance_books")
|
||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
||||
|
||||
@@ -285,6 +285,7 @@ class AssetDepreciationSchedule(Document):
|
||||
number_of_pending_depreciations = final_number_of_depreciations - start
|
||||
yearly_opening_wdv = value_after_depreciation
|
||||
current_fiscal_year_end_date = None
|
||||
prev_per_day_depr = True
|
||||
for n in range(start, final_number_of_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row:
|
||||
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
|
||||
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
||||
else:
|
||||
prev_depreciation_amount = 0
|
||||
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||
self,
|
||||
asset_doc,
|
||||
value_after_depreciation,
|
||||
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
number_of_pending_depreciations,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
if not has_pro_rata or (
|
||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||
@@ -362,6 +363,16 @@ class AssetDepreciationSchedule(Document):
|
||||
row.depreciation_start_date,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations."
|
||||
).format(
|
||||
frappe.bold(asset_doc.gross_purchase_amount),
|
||||
frappe.bold(row.total_number_of_depreciations),
|
||||
frappe.bold(row.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
@@ -599,11 +610,12 @@ def get_depreciation_amount(
|
||||
prev_depreciation_amount=0,
|
||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||
number_of_pending_depreciations=0,
|
||||
prev_per_day_depr=0,
|
||||
):
|
||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||
return get_straight_line_or_manual_depr_amount(
|
||||
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||
)
|
||||
), None
|
||||
else:
|
||||
return get_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
@@ -614,6 +626,7 @@ def get_depreciation_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
|
||||
|
||||
@@ -637,49 +650,14 @@ def get_straight_line_or_manual_depr_amount(
|
||||
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||
if row.daily_prorata_based:
|
||||
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.number_of_depreciations_booked
|
||||
- number_of_pending_depreciations
|
||||
- 1
|
||||
)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
return get_daily_prorata_based_straight_line_depr(
|
||||
asset,
|
||||
row,
|
||||
schedule_idx,
|
||||
number_of_pending_depreciations,
|
||||
amount,
|
||||
)
|
||||
from_date = add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||
else:
|
||||
return (
|
||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||
@@ -692,40 +670,9 @@ def get_straight_line_or_manual_depr_amount(
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
)
|
||||
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(
|
||||
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
return get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
)
|
||||
|
||||
daily_depr_amount = amount / total_days
|
||||
|
||||
to_date = get_last_day(
|
||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
||||
)
|
||||
from_date = add_days(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
|
||||
else:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
@@ -734,6 +681,23 @@ def get_straight_line_or_manual_depr_amount(
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
):
|
||||
total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12
|
||||
every_year_depr = amount / total_years
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
|
||||
from_date, total_depreciable_days = _get_total_days(
|
||||
row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
|
||||
)
|
||||
return daily_depr_amount * total_depreciable_days
|
||||
|
||||
|
||||
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
||||
return (
|
||||
@@ -779,6 +743,7 @@ def get_wdv_or_dd_depr_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
):
|
||||
return get_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
@@ -788,6 +753,7 @@ def get_wdv_or_dd_depr_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
|
||||
|
||||
@@ -799,6 +765,39 @@ def get_default_wdv_or_dd_depr_amount(
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
):
|
||||
if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12:
|
||||
return _get_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
), None
|
||||
else:
|
||||
return _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
)
|
||||
|
||||
|
||||
def _get_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
):
|
||||
if cint(fb_row.frequency_of_depreciation) == 12:
|
||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
||||
@@ -825,6 +824,75 @@ def get_default_wdv_or_dd_depr_amount(
|
||||
return prev_depreciation_amount
|
||||
|
||||
|
||||
def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
asset_depr_schedule,
|
||||
prev_per_day_depr,
|
||||
):
|
||||
if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month
|
||||
if schedule_idx == 0:
|
||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100), None
|
||||
|
||||
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes
|
||||
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
|
||||
else:
|
||||
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
|
||||
else:
|
||||
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes
|
||||
return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
|
||||
else:
|
||||
return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
|
||||
|
||||
|
||||
def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
|
||||
""" "
|
||||
Returns monthly depreciation amount when year changes
|
||||
1. Calculate per day depr based on new year
|
||||
2. Calculate monthly amount based on new per day amount
|
||||
"""
|
||||
from_date, days_in_month = _get_total_days(
|
||||
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
|
||||
)
|
||||
per_day_depr = get_per_day_depr(fb_row, depreciable_value, from_date)
|
||||
return (per_day_depr * days_in_month), per_day_depr
|
||||
|
||||
|
||||
def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr):
|
||||
""" "
|
||||
Returns monthly depreciation amount based on prev per day depr
|
||||
Calculate per day depr only for the first month
|
||||
"""
|
||||
from_date, days_in_month = _get_total_days(
|
||||
fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
|
||||
)
|
||||
return (prev_per_day_depr * days_in_month), prev_per_day_depr
|
||||
|
||||
|
||||
def get_per_day_depr(
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
from_date,
|
||||
):
|
||||
to_date = add_days(add_years(from_date, 1), -1)
|
||||
total_days = date_diff(to_date, from_date) + 1
|
||||
per_day_depr = (flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days
|
||||
return per_day_depr
|
||||
|
||||
|
||||
def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation):
|
||||
from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation)
|
||||
to_date = add_months(from_date, frequency_of_depreciation)
|
||||
if is_last_day_of_the_month(depreciation_start_date):
|
||||
to_date = get_last_day(to_date)
|
||||
from_date = add_days(get_last_day(from_date), 1)
|
||||
return from_date, date_diff(to_date, from_date) + 1
|
||||
|
||||
|
||||
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
||||
asset_depr_schedules_names = []
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr
|
||||
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_depr_schedule,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,3 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
||||
|
||||
def test_daily_prorata_based_depr_on_sl_methond(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2020-01-01",
|
||||
depreciation_start_date="2020-01-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=24,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2020-01-31", 4234.97, 4234.97],
|
||||
["2020-02-29", 3961.75, 8196.72],
|
||||
["2020-03-31", 4234.97, 12431.69],
|
||||
["2020-04-30", 4098.36, 16530.05],
|
||||
["2020-05-31", 4234.97, 20765.02],
|
||||
["2020-06-30", 4098.36, 24863.38],
|
||||
["2020-07-31", 4234.97, 29098.35],
|
||||
["2020-08-31", 4234.97, 33333.32],
|
||||
["2020-09-30", 4098.36, 37431.68],
|
||||
["2020-10-31", 4234.97, 41666.65],
|
||||
["2020-11-30", 4098.36, 45765.01],
|
||||
["2020-12-31", 4234.97, 49999.98],
|
||||
["2021-01-31", 4246.58, 54246.56],
|
||||
["2021-02-28", 3835.62, 58082.18],
|
||||
["2021-03-31", 4246.58, 62328.76],
|
||||
["2021-04-30", 4109.59, 66438.35],
|
||||
["2021-05-31", 4246.58, 70684.93],
|
||||
["2021-06-30", 4109.59, 74794.52],
|
||||
["2021-07-31", 4246.58, 79041.1],
|
||||
["2021-08-31", 4246.58, 83287.68],
|
||||
["2021-09-30", 4109.59, 87397.27],
|
||||
["2021-10-31", 4246.58, 91643.85],
|
||||
["2021-11-30", 4109.59, 95753.44],
|
||||
["2021-12-31", 4246.56, 100000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Test for Written Down Value Method
|
||||
# Frequency of deprciation = 3
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2021-02-20",
|
||||
depreciation_start_date="2021-03-31",
|
||||
frequency_of_depreciation=3,
|
||||
total_number_of_depreciations=6,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2021-03-31", 4383.56, 4383.56],
|
||||
["2021-06-30", 9535.45, 13919.01],
|
||||
["2021-09-30", 9640.23, 23559.24],
|
||||
["2021-12-31", 9640.23, 33199.47],
|
||||
["2022-03-31", 9430.66, 42630.13],
|
||||
["2022-06-30", 5721.27, 48351.4],
|
||||
["2022-08-20", 51648.6, 100000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Frequency of deprciation = 6
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_6_months(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2020-02-20",
|
||||
depreciation_start_date="2020-02-29",
|
||||
frequency_of_depreciation=6,
|
||||
total_number_of_depreciations=6,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2020-02-29", 1092.90, 1092.90],
|
||||
["2020-08-31", 19944.01, 21036.91],
|
||||
["2021-02-28", 19618.83, 40655.74],
|
||||
["2021-08-31", 11966.4, 52622.14],
|
||||
["2022-02-28", 11771.3, 64393.44],
|
||||
["2022-08-31", 7179.84, 71573.28],
|
||||
["2023-02-20", 28426.72, 100000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Frequency of deprciation = 12
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_12_months(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Written Down Value",
|
||||
daily_prorata_based=1,
|
||||
available_for_use_date="2020-02-20",
|
||||
depreciation_start_date="2020-03-31",
|
||||
frequency_of_depreciation=12,
|
||||
total_number_of_depreciations=4,
|
||||
rate_of_depreciation=40,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2020-03-31", 4480.87, 4480.87],
|
||||
["2021-03-31", 38207.65, 42688.52],
|
||||
["2022-03-31", 22924.59, 65613.11],
|
||||
["2023-03-31", 13754.76, 79367.87],
|
||||
["2024-02-20", 20632.13, 100000],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
@@ -159,8 +159,9 @@ def prepare_chart_data(data, filters):
|
||||
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||
filters_filter_based_on = "Date Range"
|
||||
date_field = "purchase_date"
|
||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filtered_data = [d for d in data if d.get(date_field)]
|
||||
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
|
||||
else:
|
||||
filters_filter_based_on = filters.filter_based_on
|
||||
date_field = frappe.scrub(filters.date_based_on)
|
||||
|
||||
@@ -794,6 +794,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
"field_map": {
|
||||
"name": "po_detail",
|
||||
"parent": "purchase_order",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
@@ -902,12 +904,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
target_doc.populate_items_table()
|
||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
|
||||
@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||
args: {
|
||||
doctype: "Supplier",
|
||||
tag: args.tag,
|
||||
tag: "%" + args.tag + "%",
|
||||
},
|
||||
callback: load_suppliers,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.require("assets/erpnext/js/purchase_trends_filters.js", function () {
|
||||
frappe.query_reports["Purchase Order Trends"] = {
|
||||
filters: erpnext.get_purchase_trends_filters(),
|
||||
};
|
||||
});
|
||||
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||
|
||||
@@ -2183,10 +2183,10 @@ class AccountsController(TransactionBase):
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -332,6 +333,8 @@ class BuyingController(SubcontractingController):
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||
return
|
||||
@@ -787,7 +790,7 @@ class BuyingController(SubcontractingController):
|
||||
"supplier": self.supplier,
|
||||
"purchase_date": self.posting_date,
|
||||
"calculate_depreciation": 0,
|
||||
"purchase_receipt_amount": purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
"gross_purchase_amount": purchase_amount,
|
||||
"asset_quantity": asset_quantity,
|
||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
||||
@@ -935,3 +938,8 @@ def validate_item_type(doc, fieldname, message):
|
||||
).format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_regional_item_valuation_rate(doc):
|
||||
pass
|
||||
|
||||
@@ -352,6 +352,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
page_len = 30
|
||||
|
||||
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
||||
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
||||
@@ -422,6 +423,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
& (stock_ledger_entry.batch_no.isnotnull())
|
||||
)
|
||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||
.having(Sum(stock_ledger_entry.actual_qty) != 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
@@ -472,6 +474,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||
)
|
||||
.groupby(bundle.batch_no, bundle.warehouse)
|
||||
.having(Sum(bundle.qty) != 0)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt, format_datetime, get_datetime
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.rejected_warehouse = ""
|
||||
target_doc.warehouse = source_doc.rejected_warehouse
|
||||
target_doc.received_qty = target_doc.qty
|
||||
target_doc.return_qty_from_rejected_warehouse = 1
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if source_doc.item_code:
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
and not source_doc.use_serial_batch_fields
|
||||
):
|
||||
target_doc.set("use_serial_batch_fields", 1)
|
||||
|
||||
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
if not target_doc.get("use_serial_batch_fields"):
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if not target_doc.get(qty_field):
|
||||
continue
|
||||
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
elif target_doc.get("use_serial_batch_fields"):
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
|
||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -839,3 +841,246 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
|
||||
batches.update(get_batches_from_bundle(ids))
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
|
||||
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not available_dict:
|
||||
frappe.throw(_("No Serial / Batches are available for return"))
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
],
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
for row in batch_serial_nos:
|
||||
key = row.voucher_detail_no
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
row.qty = -1 * row.qty
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
del filters["serial_and_batch_bundle"]
|
||||
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
|
||||
pluck_field = "rejected_serial_and_batch_bundle"
|
||||
|
||||
_bundle_ids = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
if not is_rejected:
|
||||
_bundle_ids.extend(
|
||||
frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
|
||||
del filters["rejected_serial_and_batch_bundle"]
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if not d.get("serial_and_batch_bundle") and not d.get("rejected_serial_and_batch_bundle"):
|
||||
continue
|
||||
|
||||
if is_rejected:
|
||||
if d.get("return_qty_from_rejected_warehouse"):
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
for serial_no, sn_qty in data.serial_nos.items():
|
||||
if sn_qty != 0:
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
else:
|
||||
filterd_serial_batch["serial_nos"] = available_serial_nos
|
||||
|
||||
elif data.batches:
|
||||
for batch_no, batch_qty in data.batches.items():
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
batch_qty = batch_qty * -1
|
||||
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
warehouse,
|
||||
)
|
||||
|
||||
if batch_qty <= 0:
|
||||
frappe.throw(
|
||||
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
|
||||
title=_("Batch Not Available for Return"),
|
||||
)
|
||||
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
if batch_qty > qty:
|
||||
filterd_serial_batch["batches"][batch_no] = qty
|
||||
qty = 0
|
||||
else:
|
||||
filterd_serial_batch["batches"][batch_no] += batch_qty
|
||||
qty -= batch_qty
|
||||
|
||||
return filterd_serial_batch
|
||||
|
||||
|
||||
def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
return get_batch_qty(
|
||||
batch_no,
|
||||
warehouse,
|
||||
posting_date=parent_doc.posting_date,
|
||||
posting_time=parent_doc.posting_time,
|
||||
for_stock_levels=True,
|
||||
)
|
||||
|
||||
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None, qty_field=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
type_of_transaction = "Outward"
|
||||
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
if not child_doc.get(qty_field):
|
||||
frappe.throw(
|
||||
_("For the {0}, the quantity is required to make the return entry").format(
|
||||
frappe.bold(child_doc.item_code)
|
||||
)
|
||||
)
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"item_code": child_doc.item_code,
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
"qty": child_doc.get(qty_field),
|
||||
"company": parent_doc.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
return cls_obj.name
|
||||
|
||||
|
||||
def get_available_serial_nos(serial_nos, warehouse):
|
||||
return frappe.get_all(
|
||||
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
|
||||
)
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.controllers.sales_and_purchase_return import (
|
||||
available_serial_batch_for_return,
|
||||
filter_serial_batches,
|
||||
make_serial_batch_bundle_for_return,
|
||||
)
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
@@ -92,7 +97,7 @@ class StockController(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
|
||||
if self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
@@ -113,7 +118,11 @@ class StockController(AccountsController):
|
||||
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries(warehouse_account)
|
||||
gl_entries = (
|
||||
self.get_gl_entries(warehouse_account, via_landed_cost_voucher)
|
||||
if self.doctype == "Purchase Receipt"
|
||||
else self.get_gl_entries(warehouse_account)
|
||||
)
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
|
||||
def validate_serialized_batch(self):
|
||||
@@ -205,6 +214,7 @@ class StockController(AccountsController):
|
||||
"company": self.company,
|
||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
"via_landed_cost_voucher": via_landed_cost_voucher,
|
||||
"do_not_submit": True if not via_landed_cost_voucher else False,
|
||||
}
|
||||
|
||||
@@ -216,6 +226,132 @@ class StockController(AccountsController):
|
||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||
if not self.get("is_return"):
|
||||
return
|
||||
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
|
||||
self.make_bundle_for_non_rejected_qty(table_name)
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
self.make_bundle_for_rejected_qty(table_name)
|
||||
|
||||
def make_bundle_for_rejected_qty(self, table_name=None):
|
||||
field, reference_ids = self.get_reference_ids(
|
||||
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(
|
||||
field, child_doctype, reference_ids, is_rejected=True
|
||||
)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
qty_field = "rejected_qty"
|
||||
warehouse_field = "rejected_warehouse"
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
qty_field = "qty"
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
if not data.get("qty"):
|
||||
frappe.throw(
|
||||
_("For the {0}, no stock is available for the return in the warehouse {1}.").format(
|
||||
frappe.bold(row.item_code), row.get(warehouse_field)
|
||||
)
|
||||
)
|
||||
|
||||
data = filter_serial_batches(
|
||||
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||
)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field, qty_field)
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def make_bundle_for_non_rejected_qty(self, table_name):
|
||||
field, reference_ids = self.get_reference_ids(table_name)
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Delivery Note": "dn_detail",
|
||||
"Purchase Receipt": "purchase_receipt_item",
|
||||
"Purchase Invoice": "purchase_invoice_item",
|
||||
"POS Invoice": "pos_invoice_item",
|
||||
}.get(self.doctype)
|
||||
|
||||
if not bundle_field:
|
||||
bundle_field = "serial_and_batch_bundle"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
reference_ids = []
|
||||
|
||||
for row in self.get(table_name):
|
||||
if not self.is_serial_batch_item(row.item_code):
|
||||
continue
|
||||
|
||||
if (
|
||||
row.get(field)
|
||||
and (
|
||||
qty_field == "qty"
|
||||
and not row.get("return_qty_from_rejected_warehouse")
|
||||
or qty_field == "rejected_qty"
|
||||
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
|
||||
)
|
||||
and not row.get("use_serial_batch_fields")
|
||||
and not row.get(bundle_field)
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
def is_serial_batch_item(self, item_code) -> bool:
|
||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if item_details.has_serial_no or item_details.has_batch_no:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -590,6 +726,9 @@ class StockController(AccountsController):
|
||||
|
||||
row.db_set("rejected_serial_and_batch_bundle", None)
|
||||
|
||||
if row.get("current_serial_and_batch_bundle"):
|
||||
row.db_set("current_serial_and_batch_bundle", None)
|
||||
|
||||
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
@@ -610,35 +749,16 @@ class StockController(AccountsController):
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||
):
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
|
||||
|
||||
if not type_of_transaction:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = warehouse
|
||||
bundle_doc.type_of_transaction = type_of_transaction
|
||||
bundle_doc.voucher_type = self.doctype
|
||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
return make_bundle_for_material_transfer(
|
||||
is_new=self.is_new(),
|
||||
docstatus=self.docstatus,
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||
warehouse=warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
sl_dict = frappe._dict(
|
||||
@@ -1556,3 +1676,38 @@ def create_item_wise_repost_entries(
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
return repost_entries
|
||||
|
||||
|
||||
def make_bundle_for_material_transfer(**kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
|
||||
|
||||
if not kwargs.type_of_transaction:
|
||||
kwargs.type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = kwargs.warehouse
|
||||
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||
bundle_doc.voucher_type = kwargs.voucher_type
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if kwargs.type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = kwargs.warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
|
||||
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.serial_no:
|
||||
if row.serial_no and not consumed_bundles.serial_nos:
|
||||
self.available_materials[key]["serial_no"] = list(
|
||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.batch_no:
|
||||
if row.batch_no and not consumed_bundles.batch_nos:
|
||||
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
||||
|
||||
def get_available_materials(self):
|
||||
|
||||
@@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
self.title = self.customer_name
|
||||
|
||||
self.calculate_totals()
|
||||
|
||||
def on_update(self):
|
||||
self.update_prospect()
|
||||
|
||||
def map_fields(self):
|
||||
|
||||
@@ -591,13 +591,13 @@
|
||||
"default": "0",
|
||||
"fieldname": "fg_based_operating_cost",
|
||||
"fieldtype": "Check",
|
||||
"label": "FG based Operating Cost"
|
||||
"label": "Finished Goods based Operating Cost"
|
||||
},
|
||||
{
|
||||
"depends_on": "fg_based_operating_cost",
|
||||
"fieldname": "fg_based_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "FG Based Operating Cost Section"
|
||||
"label": "Finished Goods Based Operating Cost"
|
||||
},
|
||||
{
|
||||
"depends_on": "fg_based_operating_cost",
|
||||
@@ -637,7 +637,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-02 16:22:47.518411",
|
||||
"modified": "2024-06-03 16:24:47.518411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -80,6 +80,18 @@ class BOMCreator(Document):
|
||||
if row.is_expandable and row.item_code == self.item_code:
|
||||
frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
|
||||
|
||||
if not row.parent_row_no and row.fg_item and row.fg_item != self.item_code:
|
||||
frappe.throw(
|
||||
_("At row {0}: set Parent Row No for item {1}").format(row.idx, row.item_code),
|
||||
title=_("Set Parent Row No in Items Table"),
|
||||
)
|
||||
|
||||
elif row.parent_row_no and row.fg_item == self.item_code:
|
||||
frappe.throw(
|
||||
_("At row {0}: Parent Row No cannot be set for item {1}").format(row.idx, row.item_code),
|
||||
title=_("Remove Parent Row No in Items Table"),
|
||||
)
|
||||
|
||||
def set_status(self, save=False):
|
||||
self.status = {
|
||||
0: "Draft",
|
||||
@@ -410,6 +422,10 @@ def add_sub_assembly(**kwargs):
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
||||
if parent_row_no:
|
||||
parent_row_no = parent_row_no[0]
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"fieldname": "fg_item",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "FG Item",
|
||||
"label": "Finished Goods Item",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -203,7 +203,7 @@
|
||||
{
|
||||
"fieldname": "fg_reference_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "FG Reference",
|
||||
"label": "Finished Goods Reference",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -230,7 +230,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-16 13:34:06.321061",
|
||||
"modified": "2024-06-03 18:45:24.339532",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Creator Item",
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation Time ",
|
||||
"label": "Operation Time",
|
||||
"oldfieldname": "time_in_mins",
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
@@ -203,4 +203,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,11 @@ class JobCard(Document):
|
||||
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
|
||||
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
|
||||
|
||||
data = self.get_overlap_for(d)
|
||||
open_job_cards = []
|
||||
if d.get("employee"):
|
||||
open_job_cards = self.get_open_job_cards(d.get("employee"))
|
||||
|
||||
data = self.get_overlap_for(d, open_job_cards=open_job_cards)
|
||||
if data:
|
||||
frappe.throw(
|
||||
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
|
||||
@@ -235,12 +239,12 @@ class JobCard(Document):
|
||||
for row in self.sub_operations:
|
||||
self.total_completed_qty += row.completed_qty
|
||||
|
||||
def get_overlap_for(self, args):
|
||||
def get_overlap_for(self, args, open_job_cards=None):
|
||||
time_logs = []
|
||||
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
||||
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
|
||||
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", open_job_cards=open_job_cards))
|
||||
|
||||
if not time_logs:
|
||||
return {}
|
||||
@@ -304,7 +308,7 @@ class JobCard(Document):
|
||||
return True
|
||||
return overlap
|
||||
|
||||
def get_time_logs(self, args, doctype):
|
||||
def get_time_logs(self, args, doctype, open_job_cards=None):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType(doctype)
|
||||
|
||||
@@ -341,8 +345,14 @@ class JobCard(Document):
|
||||
if self.workstation:
|
||||
query = query.where(jc.workstation == self.workstation)
|
||||
|
||||
if args.get("employee") and doctype == "Job Card Time Log":
|
||||
query = query.where(jctl.employee == args.get("employee"))
|
||||
if args.get("employee"):
|
||||
if not open_job_cards and doctype == "Job Card Scheduled Time":
|
||||
return []
|
||||
|
||||
if doctype == "Job Card Time Log":
|
||||
query = query.where(jctl.employee == args.get("employee"))
|
||||
else:
|
||||
query = query.where(jc.name.isin(open_job_cards))
|
||||
|
||||
if doctype != "Job Card Time Log":
|
||||
query = query.where(jc.total_time_in_mins == 0)
|
||||
@@ -351,6 +361,27 @@ class JobCard(Document):
|
||||
|
||||
return time_logs
|
||||
|
||||
def get_open_job_cards(self, employee):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(jc)
|
||||
.left_join(jctl)
|
||||
.on(jc.name == jctl.parent)
|
||||
.select(jc.name)
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (jc.workstation == self.workstation)
|
||||
& (jctl.employee == employee)
|
||||
& (jc.docstatus < 1)
|
||||
& (jc.name != self.name)
|
||||
)
|
||||
)
|
||||
|
||||
jobs = query.run(as_dict=True)
|
||||
return [job.get("name") for job in jobs] if jobs else []
|
||||
|
||||
def get_workstation_based_on_available_slot(self, existing_time_logs) -> dict:
|
||||
workstations = get_workstations(self.workstation_type)
|
||||
if workstations:
|
||||
|
||||
@@ -42,8 +42,7 @@
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Qty",
|
||||
"reqd": 1
|
||||
"label": "Completed Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
@@ -64,7 +63,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-23 14:30:00.970916",
|
||||
"modified": "2024-05-21 12:40:55.765860",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Time Log",
|
||||
@@ -74,4 +73,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,13 +400,20 @@ frappe.ui.form.on("Production Plan", {
|
||||
},
|
||||
|
||||
download_materials_required(frm) {
|
||||
const warehouses_data = [];
|
||||
|
||||
if (frm.doc.for_warehouse) {
|
||||
warehouses_data.push({ warehouse: frm.doc.for_warehouse });
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{
|
||||
fieldname: "warehouses",
|
||||
fieldtype: "Table MultiSelect",
|
||||
label: __("Warehouses"),
|
||||
default: frm.doc.from_warehouse,
|
||||
default: warehouses_data,
|
||||
options: "Production Plan Material Request Warehouse",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -452,6 +452,10 @@ class ProductionPlan(Document):
|
||||
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
||||
)
|
||||
|
||||
bom_no = data.bom_no or item_details and item_details.bom_no or ""
|
||||
if not bom_no:
|
||||
continue
|
||||
|
||||
pi = self.append(
|
||||
"po_items",
|
||||
{
|
||||
@@ -459,7 +463,7 @@ class ProductionPlan(Document):
|
||||
"item_code": data.item_code,
|
||||
"description": data.description or item_details.description,
|
||||
"stock_uom": item_details and item_details.stock_uom or "",
|
||||
"bom_no": data.bom_no or item_details and item_details.bom_no or "",
|
||||
"bom_no": bom_no,
|
||||
"planned_qty": data.pending_qty,
|
||||
"pending_qty": data.pending_qty,
|
||||
"planned_start_date": now_datetime(),
|
||||
|
||||
@@ -328,6 +328,28 @@ class TestProductionPlan(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
|
||||
|
||||
def test_production_plan_with_non_active_bom_item(self):
|
||||
item = make_item("Test Production Item 1 for Non Active BOM", {"is_stock_item": 1}).name
|
||||
|
||||
so1 = make_sales_order(item_code=item, qty=1)
|
||||
|
||||
pln = frappe.new_doc("Production Plan")
|
||||
pln.company = so1.company
|
||||
pln.get_items_from = "Sales Order"
|
||||
pln.append(
|
||||
"sales_orders",
|
||||
{
|
||||
"sales_order": so1.name,
|
||||
"sales_order_date": so1.transaction_date,
|
||||
"customer": so1.customer,
|
||||
"grand_total": so1.grand_total,
|
||||
},
|
||||
)
|
||||
|
||||
pln.get_items()
|
||||
|
||||
self.assertFalse(pln.po_items)
|
||||
|
||||
def test_production_plan_combine_items(self):
|
||||
"Test combining FG items in Production Plan."
|
||||
item = "Test Production Item 1"
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "FG Warehouse",
|
||||
"label": "Finished Goods Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -220,7 +220,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-27 13:24:43.571844",
|
||||
"modified": "2024-06-03 13:10:20.252166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
|
||||
@@ -21,7 +21,8 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
exploded_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
filters={"parent": bom},
|
||||
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"],
|
||||
fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom", "idx"],
|
||||
order_by="idx ASC",
|
||||
)
|
||||
|
||||
for item in exploded_items:
|
||||
|
||||
@@ -93,4 +93,11 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
|
||||
},
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (column.fieldname === "item_code" && value.includes("Total Quantity")) {
|
||||
value = "<strong>" + value + "</strong>";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -144,7 +144,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
|
||||
total_row = {"item_code": _("Total Quantity")}
|
||||
|
||||
for value in self.data:
|
||||
for period in self.period_list:
|
||||
|
||||
@@ -102,7 +102,12 @@ def get_columns() -> Columns:
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
|
||||
{
|
||||
"label": _("Finished Goods Value"),
|
||||
"fieldname": "total_fg_value",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{
|
||||
"label": _("Raw Material Value"),
|
||||
"fieldname": "total_rm_value",
|
||||
|
||||
@@ -363,3 +363,5 @@ erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
|
||||
@@ -13,8 +13,9 @@ def execute():
|
||||
for d in accounting_dimensions:
|
||||
doctype = "Asset Repair"
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
docfield = frappe.db.get_value("DocField", {"parent": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
if field or docfield:
|
||||
continue
|
||||
|
||||
df = {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
pr_table = frappe.qb.DocType("Pricing Rule")
|
||||
(
|
||||
frappe.qb.update(pr_table)
|
||||
.set(pr_table.has_priority, 1)
|
||||
.where((pr_table.priority.isnotnull()) & (pr_table.priority != ""))
|
||||
).run()
|
||||
@@ -0,0 +1,8 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("assets", "doctype", "asset")
|
||||
if frappe.db.has_column("Asset", "purchase_receipt_amount"):
|
||||
rename_field("Asset", "purchase_receipt_amount", "purchase_amount")
|
||||
@@ -55,6 +55,14 @@ frappe.ui.form.on("Project", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -1246,8 +1246,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
qty(doc, cdt, cdn) {
|
||||
if (!this.frm.doc.__onload?.load_after_mapping) {
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (!this.is_a_mapped_document(item)) {
|
||||
// item.pricing_rules = ''
|
||||
frappe.run_serially([
|
||||
() => this.remove_pricing_rule_for_item(item),
|
||||
@@ -2295,6 +2295,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
if (doc.is_return) {
|
||||
filters["is_return"] = 1;
|
||||
if (["Sales Invoice", "Delivery Note"].includes(doc.doctype)) {
|
||||
filters["is_inward"] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.warehouse) filters["warehouse"] = item.warehouse;
|
||||
|
||||
@@ -34,5 +34,7 @@ import "./utils/sales_common.js";
|
||||
import "./controllers/buying.js";
|
||||
import "./utils/demo.js";
|
||||
import "./financial_statements.js";
|
||||
import "./sales_trends_filters.js";
|
||||
import "./purchase_trends_filters.js";
|
||||
|
||||
// import { sum } from 'frappe/public/utils/util.js'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
erpnext.get_purchase_trends_filters = function () {
|
||||
return [
|
||||
erpnext.purchase_trends_filters = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
@@ -63,5 +63,5 @@ erpnext.get_purchase_trends_filters = function () {
|
||||
options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }],
|
||||
default: "",
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
erpnext.get_sales_trends_filters = function () {
|
||||
return [
|
||||
erpnext.sales_trends_filters = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "period",
|
||||
label: __("Period"),
|
||||
@@ -53,5 +53,5 @@ erpnext.get_sales_trends_filters = function () {
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
];
|
||||
],
|
||||
};
|
||||
|
||||
@@ -933,7 +933,13 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
frappe.msgprint(__("Please select {0}", [opts.source_doctype]));
|
||||
return;
|
||||
}
|
||||
opts.source_name = values;
|
||||
|
||||
if (values.constructor === Array) {
|
||||
opts.source_name = [...new Set(values)];
|
||||
} else {
|
||||
opts.source_name = values;
|
||||
}
|
||||
|
||||
if (
|
||||
opts.allow_child_item_selection ||
|
||||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
||||
@@ -1183,4 +1189,39 @@ $.extend(erpnext.stock.utils, {
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
||||
barcode_scanner.scan_api_call(child_row.barcode, callback);
|
||||
},
|
||||
|
||||
get_serial_range(range_string, separator) {
|
||||
/* Return an array of serial numbers generated from a range string.
|
||||
|
||||
Examples (using separator "::"):
|
||||
- "1::5" => ["1", "2", "3", "4", "5"]
|
||||
- "SN0009::12" => ["SN0009", "SN0010", "SN0011", "SN0012"]
|
||||
- "ABC//05::8" => ["ABC//05", "ABC//06", "ABC//07", "ABC//08"]
|
||||
*/
|
||||
if (!range_string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [start_str, end_str] = range_string.trim().split(separator);
|
||||
|
||||
if (!start_str || !end_str) {
|
||||
return;
|
||||
}
|
||||
|
||||
const end_int = parseInt(end_str);
|
||||
const length_difference = start_str.length - end_str.length;
|
||||
const start_int = parseInt(start_str.substring(length_difference));
|
||||
|
||||
if (isNaN(start_int) || isNaN(end_int)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serial_numbers = Array(end_int - start_int + 1)
|
||||
.fill(1)
|
||||
.map((x, y) => x + y)
|
||||
.map((x) => x + start_int - 1);
|
||||
return serial_numbers.map((val) => {
|
||||
return start_str.substring(0, length_difference) + val.toString().padStart(end_str.length, "0");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user