mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 14:39:19 +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
|
rev: v0.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: "Run ruff linter and apply fixes"
|
name: "Run ruff import sorter"
|
||||||
args: ["--fix"]
|
args: ["--select=I", "--fix"]
|
||||||
|
|
||||||
|
- id: ruff
|
||||||
|
name: "Run ruff linter"
|
||||||
|
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
name: "Format Python code"
|
name: "Run ruff formatter"
|
||||||
|
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
autoupdate_schedule: weekly
|
autoupdate_schedule: weekly
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
|
|||||||
// hide fields if group
|
// hide fields if group
|
||||||
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
|
||||||
|
|
||||||
// disable fields
|
frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
|
||||||
frm.toggle_enable(["is_group", "company"], false);
|
|
||||||
|
|
||||||
if (cint(frm.doc.is_group) == 0) {
|
if (cint(frm.doc.is_group) == 0) {
|
||||||
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
|
||||||
|
|||||||
@@ -55,8 +55,7 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Account Number",
|
"label": "Account Number"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -72,7 +71,6 @@
|
|||||||
"oldfieldname": "company",
|
"oldfieldname": "company",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 1,
|
"remember_last_selected_value": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ frappe.treeview_settings["Account"] = {
|
|||||||
"General Ledger",
|
"General Ledger",
|
||||||
"Balance Sheet",
|
"Balance Sheet",
|
||||||
"Profit and Loss Statement",
|
"Profit and Loss Statement",
|
||||||
"Cash Flow Statement",
|
"Cash Flow",
|
||||||
"Accounts Payable",
|
"Accounts Payable",
|
||||||
"Accounts Receivable",
|
"Accounts Receivable",
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -1525,7 +1525,8 @@
|
|||||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||||
"Clients cr\u00e9diteurs": {
|
"Clients cr\u00e9diteurs": {
|
||||||
"Clients - Avances et acomptes re\u00e7us sur commandes": {
|
"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": {
|
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
|
||||||
"account_number": "4196"
|
"account_number": "4196"
|
||||||
@@ -3141,4 +3142,4 @@
|
|||||||
"account_number": "7"
|
"account_number": "7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,23 @@
|
|||||||
|
|
||||||
frappe.ui.form.on("Accounts Settings", {
|
frappe.ui.form.on("Accounts Settings", {
|
||||||
refresh: function (frm) {},
|
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",
|
"unlink_advance_payment_on_cancelation_of_order",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
"delete_linked_ledger_entries",
|
"delete_linked_ledger_entries",
|
||||||
|
"enable_immutable_ledger",
|
||||||
"invoicing_features_section",
|
"invoicing_features_section",
|
||||||
"check_supplier_invoice_uniqueness",
|
"check_supplier_invoice_uniqueness",
|
||||||
"automatically_fetch_payment_terms",
|
"automatically_fetch_payment_terms",
|
||||||
@@ -105,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
|
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
|
||||||
"fieldname": "check_supplier_invoice_uniqueness",
|
"fieldname": "check_supplier_invoice_uniqueness",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Check Supplier Invoice Number Uniqueness"
|
"label": "Check Supplier Invoice Number Uniqueness"
|
||||||
@@ -454,6 +455,13 @@
|
|||||||
"fieldname": "remarks_section",
|
"fieldname": "remarks_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Remarks Column Length"
|
"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",
|
"icon": "icon-cog",
|
||||||
@@ -461,7 +469,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-30 14:04:26.553554",
|
"modified": "2024-03-15 12:11:36.085158",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
@@ -490,4 +498,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class AccountsSettings(Document):
|
|||||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||||
enable_common_party_accounting: DF.Check
|
enable_common_party_accounting: DF.Check
|
||||||
enable_fuzzy_matching: DF.Check
|
enable_fuzzy_matching: DF.Check
|
||||||
|
enable_immutable_ledger: DF.Check
|
||||||
enable_party_matching: DF.Check
|
enable_party_matching: DF.Check
|
||||||
frozen_accounts_modifier: DF.Link | None
|
frozen_accounts_modifier: DF.Link | None
|
||||||
general_ledger_remarks_length: DF.Int
|
general_ledger_remarks_length: DF.Int
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
frm.add_custom_button(__("Auto Reconcile"), function () {
|
frm.add_custom_button(__("Auto Reconcile"), function () {
|
||||||
|
if (!frm.doc.bank_account) {
|
||||||
|
frappe.msgprint(__("Please select Bank Account"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -495,12 +495,12 @@ def check_matching(
|
|||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types=None,
|
||||||
from_date,
|
from_date=None,
|
||||||
to_date,
|
to_date=None,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date=None,
|
||||||
from_reference_date,
|
from_reference_date=None,
|
||||||
to_reference_date,
|
to_reference_date=None,
|
||||||
):
|
):
|
||||||
exact_match = True if "exact_match" in document_types else False
|
exact_match = True if "exact_match" in document_types else False
|
||||||
|
|
||||||
@@ -540,14 +540,14 @@ def get_queries(
|
|||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types=None,
|
||||||
from_date,
|
from_date=None,
|
||||||
to_date,
|
to_date=None,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date=None,
|
||||||
from_reference_date,
|
from_reference_date=None,
|
||||||
to_reference_date,
|
to_reference_date=None,
|
||||||
exact_match,
|
exact_match=None,
|
||||||
common_filters,
|
common_filters=None,
|
||||||
):
|
):
|
||||||
# get queries to get matching vouchers
|
# get queries to get matching vouchers
|
||||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||||
@@ -580,15 +580,15 @@ def get_matching_queries(
|
|||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types=None,
|
||||||
exact_match,
|
exact_match=None,
|
||||||
account_from_to,
|
account_from_to=None,
|
||||||
from_date,
|
from_date=None,
|
||||||
to_date,
|
to_date=None,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date=None,
|
||||||
from_reference_date,
|
from_reference_date=None,
|
||||||
to_reference_date,
|
to_reference_date=None,
|
||||||
common_filters,
|
common_filters=None,
|
||||||
):
|
):
|
||||||
queries = []
|
queries = []
|
||||||
currency = get_account_currency(bank_account)
|
currency = get_account_currency(bank_account)
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
|
|||||||
party, or_filters=or_filters, pluck="name", limit_page_length=1
|
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:
|
if party_result:
|
||||||
result = (
|
result = (
|
||||||
party,
|
party,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
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):
|
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||||
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
|
|||||||
self.validate_child_cost_centers()
|
self.validate_child_cost_centers()
|
||||||
|
|
||||||
def validate_total_allocation_percentage(self):
|
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:
|
if total_percentage != 100:
|
||||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||||
|
|||||||
@@ -141,7 +141,19 @@ class Dunning(AccountsController):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
super().on_cancel()
|
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):
|
def resolve_dunning(doc, state):
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
"icon": "fa fa-calendar",
|
"icon": "fa fa-calendar",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-30 12:35:38.645968",
|
"modified": "2024-05-27 17:29:55.560840",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Fiscal Year",
|
"name": "Fiscal Year",
|
||||||
@@ -127,6 +127,10 @@
|
|||||||
{
|
{
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Stock Manager"
|
"role": "Stock Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"read": 1,
|
||||||
|
"role": "Auditor"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.voucher_type == "Depreciation Entry"
|
self.voucher_type == "Depreciation Entry"
|
||||||
and d.reference_type == "Asset"
|
and d.reference_type == "Asset"
|
||||||
and d.reference_name
|
and d.reference_name
|
||||||
and d.account_type == "Depreciation"
|
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||||
and d.debit
|
and d.debit
|
||||||
):
|
):
|
||||||
asset = frappe.get_doc("Asset", d.reference_name)
|
asset = frappe.get_doc("Asset", d.reference_name)
|
||||||
@@ -1031,6 +1031,17 @@ class JournalEntry(AccountsController):
|
|||||||
|
|
||||||
def build_gl_map(self):
|
def build_gl_map(self):
|
||||||
gl_map = []
|
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"):
|
for d in self.get("accounts"):
|
||||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||||
r = [d.user_remark, self.remark]
|
r = [d.user_remark, self.remark]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"party",
|
"party",
|
||||||
"party_name",
|
"party_name",
|
||||||
"book_advance_payments_in_separate_party_account",
|
"book_advance_payments_in_separate_party_account",
|
||||||
|
"reconcile_on_advance_payment_date",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"bank_account",
|
"bank_account",
|
||||||
"party_bank_account",
|
"party_bank_account",
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
"custom_remarks",
|
"custom_remarks",
|
||||||
"remarks",
|
"remarks",
|
||||||
"base_in_words",
|
"base_in_words",
|
||||||
|
"is_opening",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"print_heading",
|
"print_heading",
|
||||||
@@ -750,6 +752,7 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Book Advance Payments in Separate Party Account",
|
"label": "Book Advance Payments in Separate Party Account",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -765,6 +768,26 @@
|
|||||||
"label": "In Words",
|
"label": "In Words",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 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,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -778,7 +801,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-04-11 11:25:07.366347",
|
"modified": "2024-05-31 17:07:06.197249",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -116,11 +116,13 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
self.book_advance_payments_in_separate_party_account = False
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
if self.party_type not in ("Customer", "Supplier"):
|
if self.party_type not in ("Customer", "Supplier"):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
if not frappe.db.get_value(
|
if not frappe.db.get_value(
|
||||||
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
||||||
):
|
):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Important to set this flag for the gl building logic to work properly
|
# 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 (
|
if (account_type == "Payable" and self.party_type == "Customer") or (
|
||||||
account_type == "Receivable" and self.party_type == "Supplier"
|
account_type == "Receivable" and self.party_type == "Supplier"
|
||||||
):
|
):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.references:
|
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 there are referencers other than `allowed_types`, treat this as a normal payment entry
|
||||||
if reference_types - allowed_types:
|
if reference_types - allowed_types:
|
||||||
self.book_advance_payments_in_separate_party_account = False
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
liability_account = get_party_account(
|
liability_account = get_party_account(
|
||||||
@@ -1249,13 +1253,16 @@ class PaymentEntry(AccountsController):
|
|||||||
"voucher_detail_no": invoice.name,
|
"voucher_detail_no": invoice.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
date_field = "posting_date"
|
if self.reconcile_on_advance_payment_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
|
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)
|
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||||
args_dict["account"] = account
|
args_dict["account"] = account
|
||||||
|
|||||||
@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.check_gl_entries()
|
self.check_gl_entries()
|
||||||
self.check_pl_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):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -1525,6 +1525,55 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(pl_entries, expected_ple)
|
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):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
for key, value in pos_occurences.items():
|
for key, value in pos_occurences.items():
|
||||||
if len(value) > 1:
|
if len(value) > 1:
|
||||||
error_list.append(
|
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:
|
if error_list:
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.apply_loyalty_points()
|
self.apply_loyalty_points()
|
||||||
self.check_phone_payments()
|
self.check_phone_payments()
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
self.make_bundle_for_sales_purchase_return()
|
||||||
self.submit_serial_batch_bundle()
|
self.submit_serial_batch_bundle()
|
||||||
|
|
||||||
if self.coupon_code:
|
if self.coupon_code:
|
||||||
|
|||||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
|
pos.reload()
|
||||||
|
|
||||||
pos_return1 = make_sales_return(pos.name)
|
pos_return1 = make_sales_return(pos.name)
|
||||||
|
|
||||||
# partial return 1
|
# partial return 1
|
||||||
pos_return1.get("items")[0].qty = -1
|
pos_return1.get("items")[0].qty = -1
|
||||||
|
pos_return1.submit()
|
||||||
|
pos_return1.reload()
|
||||||
|
|
||||||
bundle_id = frappe.get_doc(
|
bundle_id = frappe.get_doc(
|
||||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
"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()
|
bundle_id.load_from_db()
|
||||||
|
|
||||||
serial_no = bundle_id.entries[0].serial_no
|
serial_no = bundle_id.entries[0].serial_no
|
||||||
self.assertEqual(serial_no, serial_nos[0])
|
self.assertEqual(serial_no, serial_nos[0])
|
||||||
|
|
||||||
pos_return1.insert()
|
|
||||||
pos_return1.submit()
|
|
||||||
|
|
||||||
# partial return 2
|
# partial return 2
|
||||||
pos_return2 = make_sales_return(pos.name)
|
pos_return2 = make_sales_return(pos.name)
|
||||||
|
pos_return2.submit()
|
||||||
|
|
||||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
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]
|
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||||
self.assertEqual(serial_no, serial_nos[1])
|
self.assertEqual(serial_no, serial_nos[1])
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
for key, value in pos_occurences.items():
|
for key, value in pos_occurences.items():
|
||||||
if len(value) > 1:
|
if len(value) > 1:
|
||||||
error_list.append(
|
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:
|
if error_list:
|
||||||
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
|||||||
if closing_entry:
|
if closing_entry:
|
||||||
closing_entry.set_status(update=True, status="Failed")
|
closing_entry.set_status(update=True, status="Failed")
|
||||||
if isinstance(error_message, list):
|
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)
|
closing_entry.db_set("error_message", error_message)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -74,15 +74,21 @@
|
|||||||
"discount_amount",
|
"discount_amount",
|
||||||
"discount_percentage",
|
"discount_percentage",
|
||||||
"for_price_list",
|
"for_price_list",
|
||||||
"section_break_13",
|
"dynamic_condition_tab",
|
||||||
"threshold_percentage",
|
|
||||||
"priority",
|
|
||||||
"condition",
|
"condition",
|
||||||
"column_break_66",
|
"section_break_13",
|
||||||
"apply_multiple_pricing_rules",
|
"apply_multiple_pricing_rules",
|
||||||
"apply_discount_on_rate",
|
"apply_discount_on_rate",
|
||||||
|
"column_break_66",
|
||||||
|
"threshold_percentage",
|
||||||
|
"validate_pricing_rule_section",
|
||||||
"validate_applied_rule",
|
"validate_applied_rule",
|
||||||
|
"column_break_texp",
|
||||||
"rule_description",
|
"rule_description",
|
||||||
|
"priority_section",
|
||||||
|
"has_priority",
|
||||||
|
"column_break_sayg",
|
||||||
|
"priority",
|
||||||
"help_section",
|
"help_section",
|
||||||
"pricing_rule_help",
|
"pricing_rule_help",
|
||||||
"reference_section",
|
"reference_section",
|
||||||
@@ -477,7 +483,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "section_break_13",
|
"fieldname": "section_break_13",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Advanced Settings"
|
"label": "Advanced Settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -487,6 +493,7 @@
|
|||||||
"label": "Threshold for Suggestion (In Percentage)"
|
"label": "Threshold for Suggestion (In Percentage)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "has_priority",
|
||||||
"description": "Higher the number, higher the priority",
|
"description": "Higher the number, higher the priority",
|
||||||
"fieldname": "priority",
|
"fieldname": "priority",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -513,6 +520,7 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.price_or_product_discount == 'Price'",
|
"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",
|
"fieldname": "validate_applied_rule",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Validate Applied Rule"
|
"label": "Validate Applied Rule"
|
||||||
@@ -525,7 +533,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "help_section",
|
"fieldname": "help_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Help Article",
|
||||||
"options": "Simple"
|
"options": "Simple"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -603,12 +612,42 @@
|
|||||||
"fieldname": "apply_recursion_over",
|
"fieldname": "apply_recursion_over",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Apply Recursion Over (As Per Transaction UOM)"
|
"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",
|
"icon": "fa fa-gift",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-02-14 04:53:34.887358",
|
"modified": "2024-05-17 13:16:34.496704",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule",
|
"name": "Pricing Rule",
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ class PricingRule(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pricing_rule_brand.pricing_rule_brand import PricingRuleBrand
|
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 (
|
from erpnext.accounts.doctype.pricing_rule_item_code.pricing_rule_item_code import PricingRuleItemCode
|
||||||
PricingRuleItemCode,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
from erpnext.accounts.doctype.pricing_rule_item_group.pricing_rule_item_group import (
|
||||||
PricingRuleItemGroup,
|
PricingRuleItemGroup,
|
||||||
)
|
)
|
||||||
@@ -67,6 +65,7 @@ class PricingRule(Document):
|
|||||||
free_item_rate: DF.Currency
|
free_item_rate: DF.Currency
|
||||||
free_item_uom: DF.Link | None
|
free_item_uom: DF.Link | None
|
||||||
free_qty: DF.Float
|
free_qty: DF.Float
|
||||||
|
has_priority: DF.Check
|
||||||
is_cumulative: DF.Check
|
is_cumulative: DF.Check
|
||||||
is_recursive: DF.Check
|
is_recursive: DF.Check
|
||||||
item_groups: DF.Table[PricingRuleItemGroup]
|
item_groups: DF.Table[PricingRuleItemGroup]
|
||||||
@@ -156,6 +155,12 @@ class PricingRule(Document):
|
|||||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||||
|
|
||||||
def validate_mandatory(self):
|
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():
|
for apply_on, field in apply_on_dict.items():
|
||||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
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)
|
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]:
|
for doc in [si, si1]:
|
||||||
doc.delete()
|
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):
|
def test_remove_pricing_rule(self):
|
||||||
item = make_item("Water Flask")
|
item = make_item("Water Flask")
|
||||||
make_item_price("Water Flask", "_Test Price List", 100)
|
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 1")
|
||||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
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"]
|
test_dependencies = ["Campaign"]
|
||||||
|
|
||||||
@@ -1185,6 +1265,7 @@ def make_pricing_rule(**args):
|
|||||||
"priority": args.priority or 1,
|
"priority": args.priority or 1,
|
||||||
"discount_amount": args.discount_amount or 0.0,
|
"discount_amount": args.discount_amount or 0.0,
|
||||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 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"]:
|
for apply_on in ["Item Code", "Item Group", "Brand"]:
|
||||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
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):
|
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -561,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
|
|||||||
|
|
||||||
if pricing_rules:
|
if pricing_rules:
|
||||||
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, 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:
|
if not pricing_rules:
|
||||||
remove_free_item(doc)
|
remove_free_item(doc)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb
|
from frappe import _, qb
|
||||||
from frappe.model.document import Document
|
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
|
running_doc = None
|
||||||
if for_filter:
|
if for_filter:
|
||||||
if isinstance(for_filter, str):
|
if isinstance(for_filter, str):
|
||||||
for_filter = frappe.json.loads(for_filter)
|
for_filter = json.loads(for_filter)
|
||||||
|
|
||||||
running_doc = frappe.db.get_value(
|
running_doc = frappe.db.get_value(
|
||||||
"Process Payment Reconciliation",
|
"Process Payment Reconciliation",
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
|
|||||||
ageing_filters = frappe._dict(
|
ageing_filters = frappe._dict(
|
||||||
{
|
{
|
||||||
"company": doc.company,
|
"company": doc.company,
|
||||||
"report_date": doc.to_date,
|
"report_date": doc.posting_date,
|
||||||
"ageing_based_on": doc.ageing_based_on,
|
"ageing_based_on": doc.ageing_based_on,
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
"range2": 60,
|
"range2": 60,
|
||||||
|
|||||||
@@ -340,10 +340,11 @@
|
|||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 25%">30 Days</th>
|
<th style="width: 25%">0 - 30 Days</th>
|
||||||
<th style="width: 25%">60 Days</th>
|
<th style="width: 25%">30 - 60 Days</th>
|
||||||
<th style="width: 25%">90 Days</th>
|
<th style="width: 25%">60 - 90 Days</th>
|
||||||
<th style="width: 25%">120 Days</th>
|
<th style="width: 25%">90 - 120 Days</th>
|
||||||
|
<th style="width: 20%">Above 120 Days</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -352,6 +353,7 @@
|
|||||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
|
||||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
|
||||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
|
||||||
|
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -485,10 +485,12 @@ function hide_fields(doc) {
|
|||||||
|
|
||||||
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
var item_fields_stock = ["warehouse_section", "received_qty", "rejected_qty"];
|
||||||
|
|
||||||
cur_frm.fields_dict["items"].grid.set_column_disp(
|
if (cur_frm.fields_dict["items"]) {
|
||||||
item_fields_stock,
|
cur_frm.fields_dict["items"].grid.set_column_disp(
|
||||||
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
item_fields_stock,
|
||||||
);
|
cint(doc.update_stock) == 1 || cint(doc.is_return) == 1 ? true : false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cur_frm.refresh_fields();
|
cur_frm.refresh_fields();
|
||||||
}
|
}
|
||||||
@@ -675,7 +677,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
if (frm.doc.supplier) {
|
if (frm.doc.supplier) {
|
||||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
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);
|
frm.set_df_property("apply_tds", "read_only", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
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.tax_withholding_category = tds_category
|
||||||
self.set_onload("supplier_tds", 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)
|
super().set_missing_values(for_validate)
|
||||||
|
|
||||||
def validate_credit_to_acc(self):
|
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_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||||
stock_items = self.get_stock_items()
|
stock_items = self.get_stock_items()
|
||||||
|
|
||||||
asset_received_but_not_billed = None
|
self.asset_received_but_not_billed = None
|
||||||
|
|
||||||
if self.update_stock:
|
if self.update_stock:
|
||||||
self.validate_item_code()
|
self.validate_item_code()
|
||||||
@@ -531,26 +547,45 @@ class PurchaseInvoice(BuyingController):
|
|||||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
|
||||||
item.expense_account = stock_not_billed_account
|
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:
|
elif item.is_fixed_asset:
|
||||||
account_type = (
|
account = None
|
||||||
"capital_work_in_progress_account"
|
if item.pr_detail:
|
||||||
if is_cwip_accounting_enabled(item.asset_category)
|
if not self.asset_received_but_not_billed:
|
||||||
else "fixed_asset_account"
|
self.asset_received_but_not_billed = self.get_company_default(
|
||||||
)
|
"asset_received_but_not_billed"
|
||||||
asset_category_account = get_asset_category_account(
|
)
|
||||||
account_type, item=item.item_code, company=self.company
|
|
||||||
)
|
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||||
if not asset_category_account:
|
arbnb_booked_in_pr = frappe.db.get_value(
|
||||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
"GL Entry",
|
||||||
throw(
|
{
|
||||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
"voucher_type": "Purchase Receipt",
|
||||||
title=_("Missing Account"),
|
"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:
|
elif not item.expense_account and for_validate:
|
||||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
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)""",
|
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):
|
if cint(self.is_return):
|
||||||
self.status_updater.append(
|
self.status_updater.append(
|
||||||
{
|
{
|
||||||
@@ -707,6 +755,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||||
if self.update_stock == 1:
|
if self.update_stock == 1:
|
||||||
|
self.make_bundle_for_sales_purchase_return()
|
||||||
self.make_bundle_using_old_serial_batch_fields()
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
@@ -1035,10 +1084,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
if provisional_accounting_for_non_stock_items:
|
if provisional_accounting_for_non_stock_items:
|
||||||
if item.purchase_receipt:
|
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",
|
"Purchase Receipt Item",
|
||||||
item.pr_detail,
|
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(
|
provisional_account = provisional_account or self.get_company_default(
|
||||||
"default_provisional_account"
|
"default_provisional_account"
|
||||||
@@ -1072,7 +1121,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.posting_date,
|
self.posting_date,
|
||||||
provisional_account,
|
provisional_account,
|
||||||
reverse=1,
|
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():
|
if not self.is_internal_transfer():
|
||||||
@@ -1182,7 +1234,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
asset.name,
|
asset.name,
|
||||||
{
|
{
|
||||||
"gross_purchase_amount": purchase_amount,
|
"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.exceptions import InvalidCurrency
|
||||||
from erpnext.projects.doctype.project.test_project import make_project
|
from erpnext.projects.doctype.project.test_project import make_project
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
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 (
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||||
)
|
)
|
||||||
@@ -72,6 +74,31 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
# teardown
|
# teardown
|
||||||
pi.delete()
|
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):
|
def test_gl_entries_without_perpetual_inventory(self):
|
||||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||||
pi = frappe.copy_doc(test_records[0])
|
pi = frappe.copy_doc(test_records[0])
|
||||||
|
|||||||
@@ -105,6 +105,8 @@
|
|||||||
"purchase_receipt",
|
"purchase_receipt",
|
||||||
"pr_detail",
|
"pr_detail",
|
||||||
"sales_invoice_item",
|
"sales_invoice_item",
|
||||||
|
"material_request",
|
||||||
|
"material_request_item",
|
||||||
"item_weight_details",
|
"item_weight_details",
|
||||||
"weight_per_unit",
|
"weight_per_unit",
|
||||||
"total_weight",
|
"total_weight",
|
||||||
@@ -934,12 +936,34 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_vbbb",
|
"fieldname": "column_break_vbbb",
|
||||||
"fieldtype": "Column Break"
|
"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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-19 19:09:47.210965",
|
"modified": "2024-06-14 11:57:07.171700",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
@@ -949,4 +973,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class PurchaseInvoiceItem(Document):
|
|||||||
manufacturer_part_no: DF.Data | None
|
manufacturer_part_no: DF.Data | None
|
||||||
margin_rate_or_amount: DF.Float
|
margin_rate_or_amount: DF.Float
|
||||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||||
|
material_request: DF.Link | None
|
||||||
|
material_request_item: DF.Data | None
|
||||||
net_amount: DF.Currency
|
net_amount: DF.Currency
|
||||||
net_rate: DF.Currency
|
net_rate: DF.Currency
|
||||||
page_break: DF.Check
|
page_break: DF.Check
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
|
||||||
"autoname": "format:ACC-REPOST-{#####}",
|
|
||||||
"creation": "2023-07-04 13:07:32.923675",
|
"creation": "2023-07-04 13:07:32.923675",
|
||||||
"default_view": "List",
|
"default_view": "List",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -55,14 +53,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-26 14:21:27.362567",
|
"modified": "2024-06-03 17:30:37.012593",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger",
|
"name": "Repost Accounting Ledger",
|
||||||
"naming_rule": "Expression",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@@ -71,7 +70,9 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-07 14:24:13.321522",
|
"modified": "2024-06-06 13:56:37.908879",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger Settings",
|
"name": "Repost Accounting Ledger Settings",
|
||||||
@@ -30,13 +30,17 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Administrator",
|
"role": "Administrator",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"select": 1
|
"select": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
|
||||||
"creation": "2022-10-19 21:59:33.553852",
|
"creation": "2022-10-19 21:59:33.553852",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -99,13 +98,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-26 14:21:35.719727",
|
"modified": "2024-06-03 17:31:04.472279",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Payment Ledger",
|
"name": "Repost Payment Ledger",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@@ -114,7 +115,9 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -787,6 +787,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Time Sheets",
|
"label": "Time Sheets",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "Sales Invoice Timesheet",
|
"options": "Sales Invoice Timesheet",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -2187,7 +2188,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-08 18:02:28.549041",
|
"modified": "2024-06-07 16:49:32.458402",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
|
|||||||
if not self.get(table_name):
|
if not self.get(table_name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
self.make_bundle_for_sales_purchase_return(table_name)
|
||||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||||
self.update_stock_ledger()
|
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.closing_text = letter_text.get("closing_text")
|
||||||
target.language = letter_text.get("language")
|
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()
|
target.validate()
|
||||||
|
|
||||||
return get_mapped_doc(
|
return get_mapped_doc(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||||
@@ -3720,9 +3721,9 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
map_docs(
|
map_docs(
|
||||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
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,
|
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()
|
si.save().submit()
|
||||||
|
|
||||||
|
|||||||
@@ -870,7 +870,8 @@
|
|||||||
"label": "Purchase Order",
|
"label": "Purchase Order",
|
||||||
"options": "Purchase Order",
|
"options": "Purchase Order",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_92",
|
"fieldname": "column_break_92",
|
||||||
@@ -926,7 +927,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-25 15:56:44.828634",
|
"modified": "2024-05-23 16:36:18.970862",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
@@ -936,4 +937,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb
|
from frappe import _, qb
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
@@ -161,7 +163,7 @@ def get_linked_payments_for_doc(
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_unreconcile_doc_for_selection(selections=None):
|
def create_unreconcile_doc_for_selection(selections=None):
|
||||||
if selections:
|
if selections:
|
||||||
selections = frappe.json.loads(selections)
|
selections = json.loads(selections)
|
||||||
# assuming each row is a unique voucher
|
# assuming each row is a unique voucher
|
||||||
for row in selections:
|
for row in selections:
|
||||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
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
|
and make reverse gl entries by swapping debit and credit
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
immutable_ledger_enabled = is_immutable_ledger_enabled()
|
||||||
|
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
gl_entry = frappe.qb.DocType("GL Entry")
|
gl_entry = frappe.qb.DocType("GL Entry")
|
||||||
gl_entries = (
|
gl_entries = (
|
||||||
@@ -608,7 +610,6 @@ def make_reverse_gl_entries(
|
|||||||
for x in gl_entries:
|
for x in gl_entries:
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.update(gle)
|
frappe.qb.update(gle)
|
||||||
.set(gle.is_cancelled, True)
|
|
||||||
.set(gle.modified, now())
|
.set(gle.modified, now())
|
||||||
.set(gle.modified_by, frappe.session.user)
|
.set(gle.modified_by, frappe.session.user)
|
||||||
.where(
|
.where(
|
||||||
@@ -623,9 +624,14 @@ def make_reverse_gl_entries(
|
|||||||
& (gle.voucher_detail_no == x.voucher_detail_no)
|
& (gle.voucher_detail_no == x.voucher_detail_no)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not immutable_ledger_enabled:
|
||||||
|
query = query.set(gle.is_cancelled, True)
|
||||||
|
|
||||||
query.run()
|
query.run()
|
||||||
else:
|
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:
|
for entry in gl_entries:
|
||||||
new_gle = copy.deepcopy(entry)
|
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["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||||
new_gle["is_cancelled"] = 1
|
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"]:
|
if new_gle["debit"] or new_gle["credit"]:
|
||||||
make_entry(new_gle, adv_adj, "Yes")
|
make_entry(new_gle, adv_adj, "Yes")
|
||||||
|
|
||||||
@@ -736,3 +746,7 @@ def validate_allowed_dimensions(gl_entry, dimension_filter_map):
|
|||||||
),
|
),
|
||||||
InvalidAccountDimensionError,
|
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:
|
if filters.account_type:
|
||||||
conditions["account_type"] = filters.account_type
|
conditions["account_type"] = filters.account_type
|
||||||
return conditions
|
|
||||||
|
|
||||||
if filters.company:
|
if filters.company:
|
||||||
conditions["company"] = filters.company
|
conditions["company"] = filters.company
|
||||||
|
|||||||
@@ -1028,20 +1028,6 @@ class ReceivablePayableReport:
|
|||||||
fieldtype="Link",
|
fieldtype="Link",
|
||||||
options="Contact",
|
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=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder import DocType
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
|
|
||||||
@@ -75,11 +76,24 @@ def get_data(filters):
|
|||||||
asset_data = assets_details.get(d.against_voucher)
|
asset_data = assets_details.get(d.against_voucher)
|
||||||
if asset_data:
|
if asset_data:
|
||||||
if not asset_data.get("accumulated_depreciation_amount"):
|
if not asset_data.get("accumulated_depreciation_amount"):
|
||||||
asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
|
AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
|
||||||
"opening_accumulated_depreciation"
|
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:
|
else:
|
||||||
asset_data.accumulated_depreciation_amount += d.debit
|
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 = frappe._dict(asset_data)
|
||||||
row.update(
|
row.update(
|
||||||
|
|||||||
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
|
|||||||
condition = ""
|
condition = ""
|
||||||
if filters.get("asset_category"):
|
if filters.get("asset_category"):
|
||||||
condition += " and asset_category = %(asset_category)s"
|
condition += " and asset_category = %(asset_category)s"
|
||||||
|
# nosemgrep
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
SELECT asset_category,
|
SELECT a.asset_category,
|
||||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_as_on_from_date,
|
end), 0) as cost_as_on_from_date,
|
||||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_new_purchase,
|
end), 0) as cost_of_new_purchase,
|
||||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||||
and disposal_date >= %(from_date)s
|
and a.disposal_date >= %(from_date)s
|
||||||
and disposal_date <= %(to_date)s then
|
and a.disposal_date <= %(to_date)s then
|
||||||
case when status = "Sold" then
|
case when a.status = "Sold" then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_sold_asset,
|
end), 0) as cost_of_sold_asset,
|
||||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||||
and disposal_date >= %(from_date)s
|
and a.disposal_date >= %(from_date)s
|
||||||
and disposal_date <= %(to_date)s then
|
and a.disposal_date <= %(to_date)s then
|
||||||
case when status = "Scrapped" then
|
case when a.status = "Scrapped" then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_scrapped_asset
|
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}
|
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,
|
"to_date": filters.to_date,
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
if filters.get("account"):
|
if filters.get("account"):
|
||||||
filters.account = get_accounts_with_children(filters.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"):
|
if filters.get("cost_center"):
|
||||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||||
@@ -329,7 +330,7 @@ def get_accounts_with_children(accounts):
|
|||||||
else:
|
else:
|
||||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
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):
|
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"):
|
if filters.get("show_net_values_in_party_account"):
|
||||||
account_type_map = get_account_type_map(filters.get("company"))
|
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):
|
def update_value_in_dict(data, key, gle):
|
||||||
data[key].debit += gle.debit
|
data[key].debit += gle.debit
|
||||||
data[key].credit += gle.credit
|
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:
|
elif group_by_voucher_consolidated:
|
||||||
keylist = [
|
keylist = [
|
||||||
|
gle.get("posting_date"),
|
||||||
gle.get("voucher_type"),
|
gle.get("voucher_type"),
|
||||||
gle.get("voucher_no"),
|
gle.get("voucher_no"),
|
||||||
gle.get("account"),
|
gle.get("account"),
|
||||||
gle.get("party_type"),
|
gle.get("party_type"),
|
||||||
gle.get("party"),
|
gle.get("party"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if immutable_ledger:
|
||||||
|
keylist.append(gle.get("creation"))
|
||||||
|
|
||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
for dim in accounting_dimensions:
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get(dim))
|
keylist.append(gle.get(dim))
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
from pypika import Order
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||||
add_sub_total_row,
|
add_sub_total_row,
|
||||||
add_total_row,
|
add_total_row,
|
||||||
|
apply_group_by_conditions,
|
||||||
get_grand_total,
|
get_grand_total,
|
||||||
get_group_by_and_display_fields,
|
get_group_by_and_display_fields,
|
||||||
get_group_by_conditions,
|
|
||||||
get_tax_accounts,
|
get_tax_accounts,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
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)
|
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()
|
aii_account_map = get_aii_accounts()
|
||||||
if item_list:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(
|
itemised_tax, tax_columns = get_tax_accounts(
|
||||||
@@ -287,59 +288,90 @@ def get_columns(additional_table_columns, filters):
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters):
|
def apply_conditions(query, pi, pii, filters):
|
||||||
conditions = ""
|
for opts in ("company", "supplier", "mode_of_payment"):
|
||||||
|
if filters.get(opts):
|
||||||
|
query = query.where(pi[opts] == filters[opts])
|
||||||
|
|
||||||
for opts in (
|
if filters.get("from_date"):
|
||||||
("company", " and `tabPurchase Invoice`.company=%(company)s"),
|
query = query.where(pi.posting_date >= filters.get("from_date"))
|
||||||
("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
|
|
||||||
("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
|
if filters.get("to_date"):
|
||||||
("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
|
query = query.where(pi.posting_date <= filters.get("to_date"))
|
||||||
("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
|
|
||||||
("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
|
if filters.get("item_code"):
|
||||||
("item_group", " and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s"),
|
query = query.where(pii.item_code == filters.get("item_code"))
|
||||||
):
|
|
||||||
if filters.get(opts[0]):
|
if filters.get("item_group"):
|
||||||
conditions += opts[1]
|
query = query.where(pii.item_group == filters.get("item_group"))
|
||||||
|
|
||||||
if not filters.get("group_by"):
|
if not filters.get("group_by"):
|
||||||
conditions += (
|
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||||
"ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc"
|
query = query.orderby(pii.item_group, order=Order.desc)
|
||||||
)
|
|
||||||
else:
|
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):
|
def get_items(filters, additional_table_columns):
|
||||||
conditions = get_conditions(filters)
|
pi = frappe.qb.DocType("Purchase Invoice")
|
||||||
if additional_query_columns:
|
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
Item = frappe.qb.DocType("Item")
|
||||||
return frappe.db.sql(
|
query = (
|
||||||
f"""
|
frappe.qb.from_(pi)
|
||||||
select
|
.join(pii)
|
||||||
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
|
.on(pi.name == pii.parent)
|
||||||
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
|
.left_join(Item)
|
||||||
`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
|
.on(pii.item_code == Item.name)
|
||||||
`tabPurchase Invoice`.unrealized_profit_loss_account,
|
.select(
|
||||||
`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
|
pii.name,
|
||||||
`tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
|
pii.parent,
|
||||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
pi.posting_date,
|
||||||
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
|
pi.credit_to,
|
||||||
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
|
pi.company,
|
||||||
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
|
pi.supplier,
|
||||||
`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
|
pi.remarks,
|
||||||
`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {additional_query_columns}
|
pi.base_net_total,
|
||||||
from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
|
pi.unrealized_profit_loss_account,
|
||||||
where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
|
pii.item_code,
|
||||||
`tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
|
pii.description,
|
||||||
`tabPurchase Invoice`.docstatus = 1 {conditions}
|
pii.item_group,
|
||||||
""",
|
pii.item_name.as_("pi_item_name"),
|
||||||
filters,
|
pii.item_group.as_("pi_item_group"),
|
||||||
as_dict=1,
|
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():
|
def get_aii_accounts():
|
||||||
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
|
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"),
|
label: __("Warehouse"),
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Warehouse",
|
options: "Warehouse",
|
||||||
|
get_query: function () {
|
||||||
|
const company = frappe.query_report.get_filter_value("company");
|
||||||
|
return {
|
||||||
|
filters: { company: company },
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "brand",
|
fieldname: "brand",
|
||||||
@@ -48,6 +54,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
|||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Brand",
|
options: "Brand",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "item_code",
|
||||||
|
label: __("Item"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldname: "item_group",
|
fieldname: "item_group",
|
||||||
label: __("Item Group"),
|
label: __("Item Group"),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe import _
|
|||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
from frappe.utils.xlsxutils import handle_html
|
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.sales_register.sales_register import get_mode_of_payments
|
||||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
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")
|
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:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
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,
|
"company": d.company,
|
||||||
"sales_order": d.sales_order,
|
"sales_order": d.sales_order,
|
||||||
"delivery_note": d.delivery_note,
|
"delivery_note": d.delivery_note,
|
||||||
"income_account": d.unrealized_profit_loss_account
|
"income_account": get_income_account(d),
|
||||||
if d.is_internal_customer == 1
|
|
||||||
else d.income_account,
|
|
||||||
"cost_center": d.cost_center,
|
"cost_center": d.cost_center,
|
||||||
"stock_qty": d.stock_qty,
|
"stock_qty": d.stock_qty,
|
||||||
"stock_uom": d.stock_uom,
|
"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
|
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):
|
def get_columns(additional_table_columns, filters):
|
||||||
columns = []
|
columns = []
|
||||||
|
|
||||||
@@ -333,93 +341,143 @@ def get_columns(additional_table_columns, filters):
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters, additional_conditions=None):
|
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||||
conditions = ""
|
for opts in ("company", "customer"):
|
||||||
|
if filters.get(opts):
|
||||||
|
query = query.where(si[opts] == filters[opts])
|
||||||
|
|
||||||
for opts in (
|
if filters.get("from_date"):
|
||||||
("company", " and `tabSales Invoice`.company=%(company)s"),
|
query = query.where(si.posting_date >= filters.get("from_date"))
|
||||||
("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 additional_conditions:
|
if filters.get("to_date"):
|
||||||
conditions += additional_conditions
|
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||||
|
|
||||||
if filters.get("mode_of_payment"):
|
if filters.get("mode_of_payment"):
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
sales_invoice = frappe.db.get_all(
|
||||||
where parent=`tabSales Invoice`.name
|
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
|
||||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
)
|
||||||
|
query = query.where(si.name.isin(sales_invoice))
|
||||||
|
|
||||||
if filters.get("warehouse"):
|
if filters.get("warehouse"):
|
||||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||||
lft, rgt = frappe.db.get_all(
|
lft, rgt = frappe.db.get_all(
|
||||||
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
|
||||||
)[0]
|
)[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:
|
else:
|
||||||
conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
|
query = query.where(sii.warehouse == filters.get("warehouse"))
|
||||||
|
|
||||||
if filters.get("brand"):
|
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"):
|
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"):
|
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:
|
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":
|
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":
|
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":
|
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"):
|
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):
|
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:
|
if additional_query_columns:
|
||||||
additional_query_columns = "," + ",".join(additional_query_columns)
|
for column in additional_query_columns:
|
||||||
return frappe.db.sql(
|
if column.get("_doctype"):
|
||||||
"""
|
table = frappe.qb.DocType(column.get("_doctype"))
|
||||||
select
|
query = query.select(table[column.get("fieldname")])
|
||||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
|
else:
|
||||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
query = query.select(si[column.get("fieldname")])
|
||||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
|
||||||
`tabSales Invoice`.is_internal_customer,
|
if filters.get("customer"):
|
||||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
query = query.where(si.customer == filters["customer"])
|
||||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
|
||||||
`tabSales Invoice Item`.project,
|
if filters.get("customer_group"):
|
||||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
query = query.where(si.customer_group == filters["customer_group"])
|
||||||
`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,
|
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||||
`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
|
|
||||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
return query.run(as_dict=True)
|
||||||
`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
|
|
||||||
|
|
||||||
|
|
||||||
def get_delivery_notes_against_sales_order(item_list):
|
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]))
|
so_item_rows = list(set([d.so_detail for d in item_list]))
|
||||||
|
|
||||||
if so_item_rows:
|
if so_item_rows:
|
||||||
delivery_notes = frappe.db.sql(
|
dn_item = frappe.qb.DocType("Delivery Note Item")
|
||||||
"""
|
delivery_notes = (
|
||||||
select parent, so_detail
|
frappe.qb.from_(dn_item)
|
||||||
from `tabDelivery Note Item`
|
.select(dn_item.parent, dn_item.so_detail)
|
||||||
where docstatus=1 and so_detail in (%s)
|
.where(dn_item.docstatus == 1)
|
||||||
group by so_detail, parent
|
.where(dn_item.so_detail.isin(so_item_rows))
|
||||||
"""
|
.groupby(dn_item.so_detail, dn_item.parent)
|
||||||
% (", ".join(["%s"] * len(so_item_rows))),
|
.run(as_dict=True)
|
||||||
tuple(so_item_rows),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for dn in delivery_notes:
|
for dn in delivery_notes:
|
||||||
@@ -446,15 +502,16 @@ def get_delivery_notes_against_sales_order(item_list):
|
|||||||
|
|
||||||
|
|
||||||
def get_grand_total(filters, doctype):
|
def get_grand_total(filters, doctype):
|
||||||
return frappe.db.sql(
|
return flt(
|
||||||
f""" SELECT
|
frappe.db.get_value(
|
||||||
SUM(`tab{doctype}`.base_grand_total)
|
doctype,
|
||||||
FROM `tab{doctype}`
|
{
|
||||||
WHERE `tab{doctype}`.docstatus = 1
|
"docstatus": 1,
|
||||||
and posting_date between %s and %s
|
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||||
""",
|
},
|
||||||
(filters.get("from_date"), filters.get("to_date")),
|
"sum(base_grand_total)",
|
||||||
)[0][0] # nosec
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_tax_accounts(
|
def get_tax_accounts(
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
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 frappe.utils import flt, getdate
|
||||||
|
|
||||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for d in entries:
|
for d in entries:
|
||||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||||
|
payment_amount = d.amount
|
||||||
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)
|
|
||||||
|
|
||||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_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)
|
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||||
|
|
||||||
row = [
|
row = [
|
||||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
|||||||
d.party_type,
|
d.party_type,
|
||||||
d.party,
|
d.party,
|
||||||
d.posting_date,
|
d.posting_date,
|
||||||
d.against_voucher,
|
d.against_voucher_no,
|
||||||
invoice.posting_date,
|
invoice.posting_date,
|
||||||
invoice.due_date,
|
invoice.due_date,
|
||||||
d.debit,
|
d.amount,
|
||||||
d.credit,
|
|
||||||
d.remarks,
|
d.remarks,
|
||||||
d.age,
|
d.age,
|
||||||
d.range1,
|
d.range1,
|
||||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
|||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
|
||||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
if not filters.party_type:
|
conditions.append(ple.delinked.eq(0))
|
||||||
if filters.payment_type == _("Outgoing"):
|
if filters.payment_type == _("Outgoing"):
|
||||||
filters.party_type = "Supplier"
|
conditions.append(ple.party_type.eq("Supplier"))
|
||||||
else:
|
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||||
filters.party_type = "Customer"
|
else:
|
||||||
|
conditions.append(ple.party_type.eq("Customer"))
|
||||||
if filters.party_type:
|
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||||
conditions.append("party_type=%(party_type)s")
|
|
||||||
|
|
||||||
if filters.party:
|
if filters.party:
|
||||||
conditions.append("party=%(party)s")
|
conditions.append(ple.party.eq(filters.party))
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
if filters.get("from_date"):
|
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"):
|
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):
|
def get_entries(filters):
|
||||||
return frappe.db.sql(
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
"""select
|
conditions = get_conditions(filters)
|
||||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
|
||||||
from `tabGL Entry`
|
query = (
|
||||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
qb.from_(ple)
|
||||||
""".format(get_conditions(filters)),
|
.select(
|
||||||
filters,
|
ple.voucher_type,
|
||||||
as_dict=1,
|
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):
|
def get_invoice_posting_date_map(filters):
|
||||||
invoice_details = {}
|
invoice_details = {}
|
||||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
dt = (
|
||||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
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
|
invoice_details[t.name] = t
|
||||||
|
|
||||||
return invoice_details
|
return invoice_details
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// 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"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||||
frappe.query_reports["Purchase Invoice Trends"] = {
|
|
||||||
filters: erpnext.get_purchase_trends_filters(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// 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"] = $.extend({}, erpnext.sales_trends_filters);
|
||||||
frappe.query_reports["Sales Invoice Trends"] = {
|
|
||||||
filters: erpnext.get_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", [])))
|
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", [])))
|
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", [])))
|
warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", [])))
|
||||||
|
inv_customer_details = customer_details.get(inv.customer, {})
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
"voucher_type": inv.doctype,
|
"voucher_type": inv.doctype,
|
||||||
@@ -88,9 +89,9 @@ def _execute(filters, additional_table_columns=None):
|
|||||||
"customer": inv.customer,
|
"customer": inv.customer,
|
||||||
"customer_name": inv.customer_name,
|
"customer_name": inv.customer_name,
|
||||||
**get_values_for_columns(additional_table_columns, inv),
|
**get_values_for_columns(additional_table_columns, inv),
|
||||||
"customer_group": customer_details.get(inv.customer).get("customer_group"),
|
"customer_group": inv_customer_details.get("customer_group"),
|
||||||
"territory": customer_details.get(inv.customer).get("territory"),
|
"territory": inv_customer_details.get("territory"),
|
||||||
"tax_id": customer_details.get(inv.customer).get("tax_id"),
|
"tax_id": inv_customer_details.get("tax_id"),
|
||||||
"receivable_account": inv.debit_to,
|
"receivable_account": inv.debit_to,
|
||||||
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
|
"mode_of_payment": ", ".join(mode_of_payments.get(inv.name, [])),
|
||||||
"project": inv.project,
|
"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
|
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
|
||||||
):
|
):
|
||||||
if isinstance(boolean, str):
|
if isinstance(boolean, str):
|
||||||
boolean = frappe.json.loads(boolean)
|
boolean = loads(boolean)
|
||||||
|
|
||||||
fiscal_years = get_fiscal_years(
|
fiscal_years = get_fiscal_years(
|
||||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
|
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("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("asset_quantity", asset_quantity);
|
||||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||||
if (item.asset_location) {
|
if (item.asset_location) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"status",
|
"status",
|
||||||
"booked_fixed_asset",
|
"booked_fixed_asset",
|
||||||
"column_break_51",
|
"column_break_51",
|
||||||
"purchase_receipt_amount",
|
"purchase_amount",
|
||||||
"default_finance_book",
|
"default_finance_book",
|
||||||
"depr_entry_posting_status",
|
"depr_entry_posting_status",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
@@ -408,15 +408,6 @@
|
|||||||
"options": "Purchase Receipt",
|
"options": "Purchase Receipt",
|
||||||
"print_hide": 1
|
"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",
|
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
||||||
"fieldname": "purchase_invoice",
|
"fieldname": "purchase_invoice",
|
||||||
@@ -546,6 +537,15 @@
|
|||||||
"label": "Additional Asset Cost",
|
"label": "Additional Asset Cost",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "purchase_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Purchase Amount",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 72,
|
"idx": 72,
|
||||||
@@ -589,7 +589,7 @@
|
|||||||
"link_fieldname": "target_asset"
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-01-15 17:35:49.226603",
|
"modified": "2024-04-18 16:45:47.306032",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
@@ -633,4 +633,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "asset_name",
|
"title_field": "asset_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ class Asset(AccountsController):
|
|||||||
number_of_depreciations_booked: DF.Int
|
number_of_depreciations_booked: DF.Int
|
||||||
opening_accumulated_depreciation: DF.Currency
|
opening_accumulated_depreciation: DF.Currency
|
||||||
policy_number: DF.Data | None
|
policy_number: DF.Data | None
|
||||||
|
purchase_amount: DF.Currency
|
||||||
purchase_date: DF.Date | None
|
purchase_date: DF.Date | None
|
||||||
purchase_invoice: DF.Link | None
|
purchase_invoice: DF.Link | None
|
||||||
purchase_receipt: DF.Link | None
|
purchase_receipt: DF.Link | None
|
||||||
purchase_receipt_amount: DF.Currency
|
|
||||||
split_from: DF.Link | None
|
split_from: DF.Link | None
|
||||||
status: DF.Literal[
|
status: DF.Literal[
|
||||||
"Draft",
|
"Draft",
|
||||||
@@ -354,7 +354,7 @@ class Asset(AccountsController):
|
|||||||
if self.is_existing_asset:
|
if self.is_existing_asset:
|
||||||
return
|
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 = _(
|
error_message = _(
|
||||||
"Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset."
|
"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()
|
purchase_document = self.get_purchase_document()
|
||||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
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(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
@@ -704,8 +704,8 @@ class Asset(AccountsController):
|
|||||||
"against": fixed_asset_account,
|
"against": fixed_asset_account,
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||||
"posting_date": self.available_for_use_date,
|
"posting_date": self.available_for_use_date,
|
||||||
"credit": self.purchase_receipt_amount,
|
"credit": self.purchase_amount,
|
||||||
"credit_in_account_currency": self.purchase_receipt_amount,
|
"credit_in_account_currency": self.purchase_amount,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
},
|
},
|
||||||
item=self,
|
item=self,
|
||||||
@@ -719,8 +719,8 @@ class Asset(AccountsController):
|
|||||||
"against": cwip_account,
|
"against": cwip_account,
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||||
"posting_date": self.available_for_use_date,
|
"posting_date": self.available_for_use_date,
|
||||||
"debit": self.purchase_receipt_amount,
|
"debit": self.purchase_amount,
|
||||||
"debit_in_account_currency": self.purchase_receipt_amount,
|
"debit_in_account_currency": self.purchase_amount,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
},
|
},
|
||||||
item=self,
|
item=self,
|
||||||
@@ -1116,8 +1116,8 @@ def create_new_asset_after_split(asset, split_qty):
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
||||||
if asset.purchase_receipt_amount:
|
if asset.purchase_amount:
|
||||||
new_asset.purchase_receipt_amount = new_gross_purchase_amount
|
new_asset.purchase_amount = new_gross_purchase_amount
|
||||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||||
new_asset.asset_quantity = split_qty
|
new_asset.asset_quantity = split_qty
|
||||||
new_asset.split_from = asset.name
|
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"):
|
for row in new_asset.get("finance_books"):
|
||||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
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 = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||||
|
|
||||||
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
|
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")
|
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]
|
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
|
||||||
)
|
)
|
||||||
self.assertEqual(depreciation_amount, 30000)
|
self.assertEqual(depreciation_amount, 30000)
|
||||||
@@ -1698,7 +1698,7 @@ def create_asset(**args):
|
|||||||
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
|
||||||
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
"number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
|
||||||
"gross_purchase_amount": args.gross_purchase_amount or 100000,
|
"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,
|
"maintenance_required": args.maintenance_required or 0,
|
||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
"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,
|
"depreciation_start_date": args.depreciation_start_date,
|
||||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||||
"shift_based": args.shift_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):
|
def on_trash(self):
|
||||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||||
super(AssetCapitalization, self).on_trash()
|
super().on_trash()
|
||||||
|
|
||||||
def cancel_target_asset(self):
|
def cancel_target_asset(self):
|
||||||
if self.entry_type == "Capitalization" and self.target_asset:
|
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.available_for_use_date = self.posting_date
|
||||||
asset_doc.purchase_date = self.posting_date
|
asset_doc.purchase_date = self.posting_date
|
||||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
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.purchase_receipt_amount = total_target_asset_value
|
|
||||||
asset_doc.capitalized_in = self.name
|
asset_doc.capitalized_in = self.name
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.flags.asset_created_via_asset_capitalization = 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 = frappe.get_doc("Asset", self.target_asset)
|
||||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
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.capitalized_in = self.name
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.save()
|
asset_doc.save()
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
# Test Target Asset values
|
# Test Target Asset values
|
||||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
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
|
# Test Consumed Asset values
|
||||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||||
@@ -179,7 +179,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
# Test Target Asset values
|
# Test Target Asset values
|
||||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
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
|
# Test Consumed Asset values
|
||||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||||
@@ -256,7 +256,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
# Test Target Asset values
|
# Test Target Asset values
|
||||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
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
|
# Test General Ledger Entries
|
||||||
expected_gle = {
|
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.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||||
|
|
||||||
asset.gross_purchase_amount = args.asset_value or 100000
|
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 = asset.append("finance_books")
|
||||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
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
|
number_of_pending_depreciations = final_number_of_depreciations - start
|
||||||
yearly_opening_wdv = value_after_depreciation
|
yearly_opening_wdv = value_after_depreciation
|
||||||
current_fiscal_year_end_date = None
|
current_fiscal_year_end_date = None
|
||||||
|
prev_per_day_depr = True
|
||||||
for n in range(start, final_number_of_depreciations):
|
for n in range(start, final_number_of_depreciations):
|
||||||
# If depreciation is already completed (for double declining balance)
|
# If depreciation is already completed (for double declining balance)
|
||||||
if skip_row:
|
if skip_row:
|
||||||
@@ -301,8 +302,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
||||||
else:
|
else:
|
||||||
prev_depreciation_amount = 0
|
prev_depreciation_amount = 0
|
||||||
|
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
|
||||||
depreciation_amount = get_depreciation_amount(
|
|
||||||
self,
|
self,
|
||||||
asset_doc,
|
asset_doc,
|
||||||
value_after_depreciation,
|
value_after_depreciation,
|
||||||
@@ -312,6 +312,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
number_of_pending_depreciations,
|
number_of_pending_depreciations,
|
||||||
|
prev_per_day_depr,
|
||||||
)
|
)
|
||||||
if not has_pro_rata or (
|
if not has_pro_rata or (
|
||||||
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
|
||||||
@@ -362,6 +363,16 @@ class AssetDepreciationSchedule(Document):
|
|||||||
row.depreciation_start_date,
|
row.depreciation_start_date,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
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:
|
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)):
|
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||||
from_date = get_last_day(
|
from_date = get_last_day(
|
||||||
@@ -599,11 +610,12 @@ def get_depreciation_amount(
|
|||||||
prev_depreciation_amount=0,
|
prev_depreciation_amount=0,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata=False,
|
has_wdv_or_dd_non_yearly_pro_rata=False,
|
||||||
number_of_pending_depreciations=0,
|
number_of_pending_depreciations=0,
|
||||||
|
prev_per_day_depr=0,
|
||||||
):
|
):
|
||||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||||
return get_straight_line_or_manual_depr_amount(
|
return get_straight_line_or_manual_depr_amount(
|
||||||
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||||
)
|
), None
|
||||||
else:
|
else:
|
||||||
return get_wdv_or_dd_depr_amount(
|
return get_wdv_or_dd_depr_amount(
|
||||||
asset,
|
asset,
|
||||||
@@ -614,6 +626,7 @@ def get_depreciation_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
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:
|
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
|
||||||
if row.daily_prorata_based:
|
if row.daily_prorata_based:
|
||||||
amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
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
|
return get_daily_prorata_based_straight_line_depr(
|
||||||
|
asset,
|
||||||
to_date = get_last_day(
|
row,
|
||||||
add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
|
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:
|
else:
|
||||||
return (
|
return (
|
||||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
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(asset.opening_accumulated_depreciation)
|
||||||
- flt(row.expected_value_after_useful_life)
|
- flt(row.expected_value_after_useful_life)
|
||||||
)
|
)
|
||||||
|
return get_daily_prorata_based_straight_line_depr(
|
||||||
total_days = (
|
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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:
|
else:
|
||||||
return (
|
return (
|
||||||
flt(asset.gross_purchase_amount)
|
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)
|
) / 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):
|
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||||
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
||||||
return (
|
return (
|
||||||
@@ -779,6 +743,7 @@ def get_wdv_or_dd_depr_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
):
|
):
|
||||||
return get_default_wdv_or_dd_depr_amount(
|
return get_default_wdv_or_dd_depr_amount(
|
||||||
asset,
|
asset,
|
||||||
@@ -788,6 +753,7 @@ def get_wdv_or_dd_depr_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
|
prev_per_day_depr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -799,6 +765,39 @@ def get_default_wdv_or_dd_depr_amount(
|
|||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
asset_depr_schedule,
|
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:
|
if cint(fb_row.frequency_of_depreciation) == 12:
|
||||||
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
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
|
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):
|
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
||||||
asset_depr_schedules_names = []
|
asset_depr_schedules_names = []
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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.test_asset import create_asset, create_asset_data
|
||||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||||
get_asset_depr_schedule_doc,
|
get_asset_depr_schedule_doc,
|
||||||
|
get_depr_schedule,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,3 +27,136 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
|
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"):
|
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||||
filters_filter_based_on = "Date Range"
|
filters_filter_based_on = "Date Range"
|
||||||
date_field = "purchase_date"
|
date_field = "purchase_date"
|
||||||
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
filtered_data = [d for d in data if d.get(date_field)]
|
||||||
filters_to_date = max(data, key=lambda a: a.get(date_field)).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:
|
else:
|
||||||
filters_filter_based_on = filters.filter_based_on
|
filters_filter_based_on = filters.filter_based_on
|
||||||
date_field = frappe.scrub(filters.date_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": {
|
"field_map": {
|
||||||
"name": "po_detail",
|
"name": "po_detail",
|
||||||
"parent": "purchase_order",
|
"parent": "purchase_order",
|
||||||
|
"material_request": "material_request",
|
||||||
|
"material_request_item": "material_request_item",
|
||||||
"wip_composite_asset": "wip_composite_asset",
|
"wip_composite_asset": "wip_composite_asset",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
@@ -902,12 +904,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_doc.populate_items_table()
|
target_doc.populate_items_table()
|
||||||
|
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||||
|
|
||||||
if target_doc.set_warehouse:
|
if target_doc.set_warehouse:
|
||||||
for item in target_doc.items:
|
for item in target_doc.items:
|
||||||
item.warehouse = target_doc.set_warehouse
|
item.warehouse = target_doc.set_warehouse
|
||||||
else:
|
else:
|
||||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
|
||||||
if source_doc.set_warehouse:
|
if source_doc.set_warehouse:
|
||||||
for item in target_doc.items:
|
for item in target_doc.items:
|
||||||
item.warehouse = source_doc.set_warehouse
|
item.warehouse = source_doc.set_warehouse
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
|||||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||||
args: {
|
args: {
|
||||||
doctype: "Supplier",
|
doctype: "Supplier",
|
||||||
tag: args.tag,
|
tag: "%" + args.tag + "%",
|
||||||
},
|
},
|
||||||
callback: load_suppliers,
|
callback: load_suppliers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// 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"] = $.extend({}, erpnext.purchase_trends_filters);
|
||||||
frappe.query_reports["Purchase Order Trends"] = {
|
|
||||||
filters: erpnext.get_purchase_trends_filters(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -2183,10 +2183,10 @@ class AccountsController(TransactionBase):
|
|||||||
for d in self.get("payment_schedule"):
|
for d in self.get("payment_schedule"):
|
||||||
if d.invoice_portion:
|
if d.invoice_portion:
|
||||||
d.payment_amount = flt(
|
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(
|
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
|
d.outstanding = d.payment_amount
|
||||||
elif not d.invoice_portion:
|
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 import cint, flt, getdate
|
||||||
from frappe.utils.data import nowtime
|
from frappe.utils.data import nowtime
|
||||||
|
|
||||||
|
import erpnext
|
||||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||||
from erpnext.accounts.party import get_party_details
|
from erpnext.accounts.party import get_party_details
|
||||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||||
@@ -332,6 +333,8 @@ class BuyingController(SubcontractingController):
|
|||||||
else:
|
else:
|
||||||
item.valuation_rate = 0.0
|
item.valuation_rate = 0.0
|
||||||
|
|
||||||
|
update_regional_item_valuation_rate(self)
|
||||||
|
|
||||||
def set_incoming_rate(self):
|
def set_incoming_rate(self):
|
||||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||||
return
|
return
|
||||||
@@ -787,7 +790,7 @@ class BuyingController(SubcontractingController):
|
|||||||
"supplier": self.supplier,
|
"supplier": self.supplier,
|
||||||
"purchase_date": self.posting_date,
|
"purchase_date": self.posting_date,
|
||||||
"calculate_depreciation": 0,
|
"calculate_depreciation": 0,
|
||||||
"purchase_receipt_amount": purchase_amount,
|
"purchase_amount": purchase_amount,
|
||||||
"gross_purchase_amount": purchase_amount,
|
"gross_purchase_amount": purchase_amount,
|
||||||
"asset_quantity": asset_quantity,
|
"asset_quantity": asset_quantity,
|
||||||
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
|
"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)
|
).format(items, message)
|
||||||
|
|
||||||
frappe.throw(error_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"
|
doctype = "Batch"
|
||||||
meta = frappe.get_meta(doctype, cached=True)
|
meta = frappe.get_meta(doctype, cached=True)
|
||||||
searchfields = meta.get_search_fields()
|
searchfields = meta.get_search_fields()
|
||||||
|
page_len = 30
|
||||||
|
|
||||||
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
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))
|
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())
|
& (stock_ledger_entry.batch_no.isnotnull())
|
||||||
)
|
)
|
||||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||||
|
.having(Sum(stock_ledger_entry.actual_qty) != 0)
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(page_len)
|
.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())
|
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||||
)
|
)
|
||||||
.groupby(bundle.batch_no, bundle.warehouse)
|
.groupby(bundle.batch_no, bundle.warehouse)
|
||||||
|
.having(Sum(bundle.qty) != 0)
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(page_len)
|
.limit(page_len)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
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
|
import erpnext
|
||||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
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.rejected_warehouse = ""
|
||||||
target_doc.warehouse = source_doc.rejected_warehouse
|
target_doc.warehouse = source_doc.rejected_warehouse
|
||||||
target_doc.received_qty = target_doc.qty
|
target_doc.received_qty = target_doc.qty
|
||||||
|
target_doc.return_qty_from_rejected_warehouse = 1
|
||||||
|
|
||||||
elif doctype == "Purchase Invoice":
|
elif doctype == "Purchase Invoice":
|
||||||
returned_qty_map = get_returned_qty_map_for_row(
|
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:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = 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_details = frappe.get_cached_value(
|
||||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
"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:
|
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not target_doc.get("use_serial_batch_fields"):
|
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||||
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)
|
|
||||||
|
|
||||||
def 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
|
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))
|
batches.update(get_batches_from_bundle(ids))
|
||||||
|
|
||||||
return batches
|
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.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
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 import get_warehouse_account_map
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||||
get_evaluated_inventory_dimension,
|
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:
|
if self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
@@ -113,7 +118,11 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
if not gl_entries:
|
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)
|
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||||
|
|
||||||
def validate_serialized_batch(self):
|
def validate_serialized_batch(self):
|
||||||
@@ -205,6 +214,7 @@ class StockController(AccountsController):
|
|||||||
"company": self.company,
|
"company": self.company,
|
||||||
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
|
||||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
"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,
|
"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.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||||
self.create_serial_batch_bundle(bundle_details, row)
|
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):
|
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
|
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)
|
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):
|
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
||||||
if not table_name:
|
if not table_name:
|
||||||
table_name = "items"
|
table_name = "items"
|
||||||
@@ -610,35 +749,16 @@ class StockController(AccountsController):
|
|||||||
def make_package_for_transfer(
|
def make_package_for_transfer(
|
||||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
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)
|
return make_bundle_for_material_transfer(
|
||||||
|
is_new=self.is_new(),
|
||||||
if not type_of_transaction:
|
docstatus=self.docstatus,
|
||||||
type_of_transaction = "Inward"
|
voucher_type=self.doctype,
|
||||||
|
voucher_no=self.name,
|
||||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||||
bundle_doc.warehouse = warehouse
|
warehouse=warehouse,
|
||||||
bundle_doc.type_of_transaction = type_of_transaction
|
type_of_transaction=type_of_transaction,
|
||||||
bundle_doc.voucher_type = self.doctype
|
do_not_submit=do_not_submit,
|
||||||
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
|
|
||||||
|
|
||||||
def get_sl_entries(self, d, args):
|
def get_sl_entries(self, d, args):
|
||||||
sl_dict = frappe._dict(
|
sl_dict = frappe._dict(
|
||||||
@@ -1556,3 +1676,38 @@ def create_item_wise_repost_entries(
|
|||||||
repost_entries.append(repost_entry)
|
repost_entries.append(repost_entry)
|
||||||
|
|
||||||
return repost_entries
|
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)
|
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
||||||
|
|
||||||
# Will be deprecated in v16
|
# 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(
|
self.available_materials[key]["serial_no"] = list(
|
||||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Will be deprecated in v16
|
# 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
|
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
||||||
|
|
||||||
def get_available_materials(self):
|
def get_available_materials(self):
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote):
|
|||||||
self.title = self.customer_name
|
self.title = self.customer_name
|
||||||
|
|
||||||
self.calculate_totals()
|
self.calculate_totals()
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
self.update_prospect()
|
self.update_prospect()
|
||||||
|
|
||||||
def map_fields(self):
|
def map_fields(self):
|
||||||
|
|||||||
@@ -591,13 +591,13 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "fg_based_operating_cost",
|
"fieldname": "fg_based_operating_cost",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "FG based Operating Cost"
|
"label": "Finished Goods based Operating Cost"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "fg_based_operating_cost",
|
"depends_on": "fg_based_operating_cost",
|
||||||
"fieldname": "fg_based_section_section",
|
"fieldname": "fg_based_section_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "FG Based Operating Cost Section"
|
"label": "Finished Goods Based Operating Cost"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "fg_based_operating_cost",
|
"depends_on": "fg_based_operating_cost",
|
||||||
@@ -637,7 +637,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-02 16:22:47.518411",
|
"modified": "2024-06-03 16:24:47.518411",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
|
|||||||
@@ -80,6 +80,18 @@ class BOMCreator(Document):
|
|||||||
if row.is_expandable and row.item_code == self.item_code:
|
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))
|
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):
|
def set_status(self, save=False):
|
||||||
self.status = {
|
self.status = {
|
||||||
0: "Draft",
|
0: "Draft",
|
||||||
@@ -410,6 +422,10 @@ def add_sub_assembly(**kwargs):
|
|||||||
|
|
||||||
parent_row_no = item_row.idx
|
parent_row_no = item_row.idx
|
||||||
name = ""
|
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"):
|
for row in bom_item.get("items"):
|
||||||
row = frappe._dict(row)
|
row = frappe._dict(row)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"fieldname": "fg_item",
|
"fieldname": "fg_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "FG Item",
|
"label": "Finished Goods Item",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "fg_reference_id",
|
"fieldname": "fg_reference_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "FG Reference",
|
"label": "Finished Goods Reference",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-16 13:34:06.321061",
|
"modified": "2024-06-03 18:45:24.339532",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Creator Item",
|
"name": "BOM Creator Item",
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
"fieldname": "time_in_mins",
|
"fieldname": "time_in_mins",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Operation Time ",
|
"label": "Operation Time",
|
||||||
"oldfieldname": "time_in_mins",
|
"oldfieldname": "time_in_mins",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -203,4 +203,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,7 +214,11 @@ class JobCard(Document):
|
|||||||
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
|
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))
|
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:
|
if data:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
|
_("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:
|
for row in self.sub_operations:
|
||||||
self.total_completed_qty += row.completed_qty
|
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 = []
|
||||||
|
|
||||||
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
|
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:
|
if not time_logs:
|
||||||
return {}
|
return {}
|
||||||
@@ -304,7 +308,7 @@ class JobCard(Document):
|
|||||||
return True
|
return True
|
||||||
return overlap
|
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")
|
jc = frappe.qb.DocType("Job Card")
|
||||||
jctl = frappe.qb.DocType(doctype)
|
jctl = frappe.qb.DocType(doctype)
|
||||||
|
|
||||||
@@ -341,8 +345,14 @@ class JobCard(Document):
|
|||||||
if self.workstation:
|
if self.workstation:
|
||||||
query = query.where(jc.workstation == self.workstation)
|
query = query.where(jc.workstation == self.workstation)
|
||||||
|
|
||||||
if args.get("employee") and doctype == "Job Card Time Log":
|
if args.get("employee"):
|
||||||
query = query.where(jctl.employee == 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":
|
if doctype != "Job Card Time Log":
|
||||||
query = query.where(jc.total_time_in_mins == 0)
|
query = query.where(jc.total_time_in_mins == 0)
|
||||||
@@ -351,6 +361,27 @@ class JobCard(Document):
|
|||||||
|
|
||||||
return time_logs
|
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:
|
def get_workstation_based_on_available_slot(self, existing_time_logs) -> dict:
|
||||||
workstations = get_workstations(self.workstation_type)
|
workstations = get_workstations(self.workstation_type)
|
||||||
if workstations:
|
if workstations:
|
||||||
|
|||||||
@@ -42,8 +42,7 @@
|
|||||||
"fieldname": "completed_qty",
|
"fieldname": "completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Completed Qty",
|
"label": "Completed Qty"
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "employee",
|
"fieldname": "employee",
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-23 14:30:00.970916",
|
"modified": "2024-05-21 12:40:55.765860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card Time Log",
|
"name": "Job Card Time Log",
|
||||||
@@ -74,4 +73,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,13 +400,20 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
download_materials_required(frm) {
|
download_materials_required(frm) {
|
||||||
|
const warehouses_data = [];
|
||||||
|
|
||||||
|
if (frm.doc.for_warehouse) {
|
||||||
|
warehouses_data.push({ warehouse: frm.doc.for_warehouse });
|
||||||
|
}
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
{
|
{
|
||||||
fieldname: "warehouses",
|
fieldname: "warehouses",
|
||||||
fieldtype: "Table MultiSelect",
|
fieldtype: "Table MultiSelect",
|
||||||
label: __("Warehouses"),
|
label: __("Warehouses"),
|
||||||
default: frm.doc.from_warehouse,
|
default: warehouses_data,
|
||||||
options: "Production Plan Material Request Warehouse",
|
options: "Production Plan Material Request Warehouse",
|
||||||
|
reqd: 1,
|
||||||
get_query: function () {
|
get_query: function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ class ProductionPlan(Document):
|
|||||||
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
{"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(
|
pi = self.append(
|
||||||
"po_items",
|
"po_items",
|
||||||
{
|
{
|
||||||
@@ -459,7 +463,7 @@ class ProductionPlan(Document):
|
|||||||
"item_code": data.item_code,
|
"item_code": data.item_code,
|
||||||
"description": data.description or item_details.description,
|
"description": data.description or item_details.description,
|
||||||
"stock_uom": item_details and item_details.stock_uom or "",
|
"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,
|
"planned_qty": data.pending_qty,
|
||||||
"pending_qty": data.pending_qty,
|
"pending_qty": data.pending_qty,
|
||||||
"planned_start_date": now_datetime(),
|
"planned_start_date": now_datetime(),
|
||||||
|
|||||||
@@ -328,6 +328,28 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
|
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):
|
def test_production_plan_combine_items(self):
|
||||||
"Test combining FG items in Production Plan."
|
"Test combining FG items in Production Plan."
|
||||||
item = "Test Production Item 1"
|
item = "Test Production Item 1"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "FG Warehouse",
|
"label": "Finished Goods Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-27 13:24:43.571844",
|
"modified": "2024-06-03 13:10:20.252166",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Item",
|
"name": "Production Plan Item",
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
|||||||
exploded_items = frappe.get_all(
|
exploded_items = frappe.get_all(
|
||||||
"BOM Item",
|
"BOM Item",
|
||||||
filters={"parent": bom},
|
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:
|
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:
|
if not self.data:
|
||||||
return
|
return
|
||||||
|
|
||||||
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
|
total_row = {"item_code": _("Total Quantity")}
|
||||||
|
|
||||||
for value in self.data:
|
for value in self.data:
|
||||||
for period in self.period_list:
|
for period in self.period_list:
|
||||||
|
|||||||
@@ -102,7 +102,12 @@ def get_columns() -> Columns:
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": "150",
|
"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"),
|
"label": _("Raw Material Value"),
|
||||||
"fieldname": "total_rm_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.delete_orphaned_asset_movement_item_records
|
||||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
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:
|
for d in accounting_dimensions:
|
||||||
doctype = "Asset Repair"
|
doctype = "Asset Repair"
|
||||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
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
|
continue
|
||||||
|
|
||||||
df = {
|
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,
|
filters: filters,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("cost_center", () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
|||||||
@@ -1246,8 +1246,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
qty(doc, cdt, cdn) {
|
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 = ''
|
// item.pricing_rules = ''
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
() => this.remove_pricing_rule_for_item(item),
|
() => this.remove_pricing_rule_for_item(item),
|
||||||
@@ -2295,6 +2295,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
|
|
||||||
if (doc.is_return) {
|
if (doc.is_return) {
|
||||||
filters["is_return"] = 1;
|
filters["is_return"] = 1;
|
||||||
|
if (["Sales Invoice", "Delivery Note"].includes(doc.doctype)) {
|
||||||
|
filters["is_inward"] = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.warehouse) filters["warehouse"] = item.warehouse;
|
if (item.warehouse) filters["warehouse"] = item.warehouse;
|
||||||
|
|||||||
@@ -34,5 +34,7 @@ import "./utils/sales_common.js";
|
|||||||
import "./controllers/buying.js";
|
import "./controllers/buying.js";
|
||||||
import "./utils/demo.js";
|
import "./utils/demo.js";
|
||||||
import "./financial_statements.js";
|
import "./financial_statements.js";
|
||||||
|
import "./sales_trends_filters.js";
|
||||||
|
import "./purchase_trends_filters.js";
|
||||||
|
|
||||||
// import { sum } from 'frappe/public/utils/util.js'
|
// import { sum } from 'frappe/public/utils/util.js'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
erpnext.get_purchase_trends_filters = function () {
|
erpnext.purchase_trends_filters = {
|
||||||
return [
|
filters: [
|
||||||
{
|
{
|
||||||
fieldname: "company",
|
fieldname: "company",
|
||||||
label: __("Company"),
|
label: __("Company"),
|
||||||
@@ -63,5 +63,5 @@ erpnext.get_purchase_trends_filters = function () {
|
|||||||
options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }],
|
options: ["", { value: "Item", label: __("Item") }, { value: "Supplier", label: __("Supplier") }],
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
erpnext.get_sales_trends_filters = function () {
|
erpnext.sales_trends_filters = {
|
||||||
return [
|
filters: [
|
||||||
{
|
{
|
||||||
fieldname: "period",
|
fieldname: "period",
|
||||||
label: __("Period"),
|
label: __("Period"),
|
||||||
@@ -53,5 +53,5 @@ erpnext.get_sales_trends_filters = function () {
|
|||||||
options: "Company",
|
options: "Company",
|
||||||
default: frappe.defaults.get_user_default("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]));
|
frappe.msgprint(__("Please select {0}", [opts.source_doctype]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
opts.source_name = values;
|
|
||||||
|
if (values.constructor === Array) {
|
||||||
|
opts.source_name = [...new Set(values)];
|
||||||
|
} else {
|
||||||
|
opts.source_name = values;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
opts.allow_child_item_selection ||
|
opts.allow_child_item_selection ||
|
||||||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
["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 });
|
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
||||||
barcode_scanner.scan_api_call(child_row.barcode, callback);
|
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