mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-24 11:29:48 +00:00
Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0854333e42 | ||
|
|
48e0ce1132 | ||
|
|
ba7e0b9506 | ||
|
|
a94a13a7c1 | ||
|
|
e573c6094a | ||
|
|
524619e0b4 | ||
|
|
5f5fa2f8e1 | ||
|
|
65c277fd27 | ||
|
|
8371dafb1a | ||
|
|
f8bc0e47ff | ||
|
|
d3a95f04cc | ||
|
|
5f63794739 | ||
|
|
41bca34f6a | ||
|
|
f4b7093838 | ||
|
|
536264896e | ||
|
|
177b23c624 | ||
|
|
f81dba6380 | ||
|
|
b7b5f6acf3 | ||
|
|
d60d8a2625 | ||
|
|
e0efe922a6 | ||
|
|
500972e8aa | ||
|
|
b833dfab9d | ||
|
|
818ddc0b8c | ||
|
|
d0d1d63d31 | ||
|
|
f2d644ba29 | ||
|
|
9e831bbfc6 | ||
|
|
7690efa4f7 | ||
|
|
00669661e5 | ||
|
|
2ab1b42033 | ||
|
|
2c156b58f0 | ||
|
|
c617b343b3 | ||
|
|
b2de9cdef2 | ||
|
|
ae59908f0a | ||
|
|
a10e3948b2 | ||
|
|
fbe1f449d8 | ||
|
|
b3fa0ac596 | ||
|
|
82d03e2617 | ||
|
|
3f004db14f | ||
|
|
4545213adc | ||
|
|
a336e19bb8 | ||
|
|
9a99ccc166 | ||
|
|
9935f04bde | ||
|
|
cc5c4a3f9a | ||
|
|
0fefccc128 | ||
|
|
a8448f9e60 | ||
|
|
450061c7db | ||
|
|
5df8ad6ef1 | ||
|
|
6b41dc2fed | ||
|
|
ed77c15ebc | ||
|
|
d4700e5560 | ||
|
|
f4c9a6e59c | ||
|
|
f1621d15ff | ||
|
|
8638654c13 | ||
|
|
5cd36c318b | ||
|
|
8eede1d266 | ||
|
|
26db582499 | ||
|
|
89458f07c9 | ||
|
|
a4633d6e75 | ||
|
|
dbe107345c | ||
|
|
a2436e4b6e | ||
|
|
9a538c6843 | ||
|
|
d8f5924eb2 | ||
|
|
9312a5b762 | ||
|
|
edef03ac22 | ||
|
|
74948aabda | ||
|
|
cfedaf5dc1 | ||
|
|
f25097da1d | ||
|
|
3952f5d913 | ||
|
|
9d23199c25 | ||
|
|
b1abcd5577 | ||
|
|
09f866022b | ||
|
|
f087da927a | ||
|
|
171d86a3ab | ||
|
|
58c6031c1f | ||
|
|
ff362843cb | ||
|
|
86b14dd4be | ||
|
|
037ef10fcb | ||
|
|
ab20b965ca | ||
|
|
e1a726bdd4 | ||
|
|
221e5a2190 | ||
|
|
f0724f2f04 | ||
|
|
c404faaa6d | ||
|
|
abfe3c8365 | ||
|
|
61f4547860 | ||
|
|
c69bb746ce | ||
|
|
bf78f6173c | ||
|
|
e628a37b99 | ||
|
|
f1062c61f6 | ||
|
|
f479675ce6 | ||
|
|
ce7dbf3090 | ||
|
|
ac587b9c94 | ||
|
|
1ea3daeb17 | ||
|
|
b2f6fed42c | ||
|
|
352642096e | ||
|
|
ab61b46a01 | ||
|
|
9b8fffd1d4 | ||
|
|
46243fdb5f | ||
|
|
cfc8c610fa | ||
|
|
d73ff810d9 | ||
|
|
2b37287b19 | ||
|
|
f31b008502 | ||
|
|
6f24c02121 | ||
|
|
a1eab1db74 | ||
|
|
fcf9f82092 | ||
|
|
e817561dce | ||
|
|
a49026e9d2 | ||
|
|
7574069af2 | ||
|
|
0e2bca4b34 | ||
|
|
7de15b74d4 | ||
|
|
a0db227a7a | ||
|
|
af55ce0f6c | ||
|
|
894bb703f6 | ||
|
|
b6e09531d7 | ||
|
|
8f47505604 | ||
|
|
5463a8b6cf | ||
|
|
b8a773e3e1 | ||
|
|
dc642fbc41 | ||
|
|
ddbdcbb026 | ||
|
|
df938f24d4 | ||
|
|
bde63ed0e5 | ||
|
|
1c3ac9c1fd | ||
|
|
0c07dfadfe | ||
|
|
1f7eccdac5 | ||
|
|
ef202d7cd0 | ||
|
|
de03618b09 | ||
|
|
ecd30c5809 | ||
|
|
41d22d0255 | ||
|
|
a913cfea7e | ||
|
|
a6c5738f4b | ||
|
|
3a6956ba05 | ||
|
|
1deadb8daa | ||
|
|
463ad0a163 | ||
|
|
f6bb86574e | ||
|
|
889a14b557 | ||
|
|
0b96e1e3ef | ||
|
|
24f892d582 | ||
|
|
651b9521b9 | ||
|
|
c2c5e45bc6 | ||
|
|
1ccdb67c55 | ||
|
|
2bf8dffb60 | ||
|
|
6511eb4c7c | ||
|
|
5aed023434 | ||
|
|
764c81a146 | ||
|
|
2d7a7d9988 | ||
|
|
ac22c422c8 | ||
|
|
f560286610 | ||
|
|
19a6ce3605 | ||
|
|
4341ac7e7a | ||
|
|
866780b383 | ||
|
|
ca2f4e801e | ||
|
|
f8cfbda4e0 | ||
|
|
ad40bfe4ea | ||
|
|
5cabdbfe06 | ||
|
|
153ed04161 | ||
|
|
78013d3d8f | ||
|
|
b6dda08290 | ||
|
|
1ea876bc7a | ||
|
|
b92662a095 | ||
|
|
ceab26d5f1 | ||
|
|
ad0819feee | ||
|
|
c5bf889391 | ||
|
|
7303ee4bea | ||
|
|
b3d337a45b | ||
|
|
991ddfe187 | ||
|
|
881dcf817f | ||
|
|
c07d5b6dec | ||
|
|
4c2f555379 | ||
|
|
27b5d9493a | ||
|
|
b3c43e8527 | ||
|
|
1005ee64cd | ||
|
|
8150638519 | ||
|
|
194e15fe6e | ||
|
|
1223f5551f | ||
|
|
f0ddf1b223 | ||
|
|
bd128d3b3a | ||
|
|
42460980b2 | ||
|
|
0958a3b643 | ||
|
|
f5adfe3f0f | ||
|
|
4e70005937 | ||
|
|
4eff2b58e6 | ||
|
|
7c2bf026ef | ||
|
|
6896216276 | ||
|
|
bf61014fe5 | ||
|
|
e0ca060475 | ||
|
|
84d348b3d4 | ||
|
|
a63457e5b0 | ||
|
|
6a6130e06c | ||
|
|
3057f6ce35 | ||
|
|
7fbefef72a | ||
|
|
ea3015a450 | ||
|
|
9146bd95a4 | ||
|
|
4576fcd96f | ||
|
|
764c71d3e1 | ||
|
|
f85b08d2f5 | ||
|
|
f0ae90011b | ||
|
|
d3daeaf475 | ||
|
|
79a9d583d9 | ||
|
|
89376ddf8d | ||
|
|
d2ac603fd7 | ||
|
|
3340b19c5d | ||
|
|
b33bec4dad | ||
|
|
4d1022182d | ||
|
|
f2b9e73819 | ||
|
|
31cb78e3f3 | ||
|
|
789cd20d2d | ||
|
|
cb58d05777 | ||
|
|
ad5421a413 | ||
|
|
84f9a1fb2d | ||
|
|
7d88b8bbb9 | ||
|
|
baa08ce496 | ||
|
|
7031a5033c | ||
|
|
1f1e88d52e | ||
|
|
97f13049ee | ||
|
|
1c9032a4c2 | ||
|
|
323a91ddd7 | ||
|
|
ee30357835 | ||
|
|
4d18fd0e80 | ||
|
|
d054486495 | ||
|
|
e9365d7272 | ||
|
|
b3578ecf09 | ||
|
|
23db22ac0b | ||
|
|
85099a3a68 | ||
|
|
db9f0b6f38 | ||
|
|
56311e6e1e | ||
|
|
40504b8da2 | ||
|
|
379def5f0f | ||
|
|
ab6a534df2 | ||
|
|
7b5e2a6af0 | ||
|
|
994454bfc3 | ||
|
|
b8f1c8f6b1 | ||
|
|
dd39d24da0 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.65.1"
|
||||
__version__ = "15.69.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -138,6 +138,11 @@ frappe.treeview_settings["Account"] = {
|
||||
description: __(
|
||||
"Further accounts can be made under Groups, but entries can be made against non-Groups"
|
||||
),
|
||||
onchange: function () {
|
||||
if (!this.value) {
|
||||
this.layout.set_value("root_type", "");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
|
||||
@@ -22,4 +22,21 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
if (frm.doc[field_name]) {
|
||||
const other_field =
|
||||
field_name === "add_taxes_from_item_tax_template"
|
||||
? "add_taxes_from_taxes_and_charges_template"
|
||||
: "add_taxes_from_item_tax_template";
|
||||
frm.set_value(other_field, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"add_taxes_from_taxes_and_charges_template",
|
||||
"book_tax_discount_loss",
|
||||
"round_row_wise_tax",
|
||||
"print_settings",
|
||||
@@ -45,6 +46,7 @@
|
||||
"role_to_override_stop_action",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
@@ -592,6 +594,21 @@
|
||||
{
|
||||
"fieldname": "column_break_feyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System will do an implicit conversion using the pegged currency. <br>\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.",
|
||||
"documentation_url": "/app/pegged-currencies/Pegged Currencies",
|
||||
"fieldname": "allow_pegged_currencies_exchange_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Implicit Pegged Currency Conversion"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.",
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -599,7 +616,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-05 12:29:38.302027",
|
||||
"modified": "2025-06-23 15:55:33.346398",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -25,7 +25,9 @@ class AccountsSettings(Document):
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_pegged_currencies_exchange_rates: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
@@ -73,6 +75,7 @@ class AccountsSettings(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_tax_settings()
|
||||
old_doc = self.get_doc_before_save()
|
||||
clear_cache = False
|
||||
|
||||
@@ -139,3 +142,13 @@ class AccountsSettings(Document):
|
||||
if self.has_value_changed("reconciliation_queue_size"):
|
||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||
frappe.throw(_("Queue Size should be between 5 and 100"))
|
||||
|
||||
def validate_auto_tax_settings(self):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@@ -118,8 +118,9 @@ class BankClearance(Document):
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
from rapidfuzz.utils import default_process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
processor=default_process,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"default": "ACC-BTN-.YYYY.-",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "ACC-BTN-.YYYY.-",
|
||||
@@ -236,9 +235,10 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -287,9 +287,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "date",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"budget_against",
|
||||
"company",
|
||||
"cost_center",
|
||||
"naming_series",
|
||||
"project",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
@@ -195,19 +195,19 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-10 22:14:36.361509",
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
@@ -235,4 +235,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class Budget(Document):
|
||||
cost_center: DF.Link | None
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Data | None
|
||||
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -136,9 +136,6 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def before_naming(self):
|
||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
||||
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -6,7 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -146,8 +147,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
if not self.title:
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
@@ -239,9 +239,10 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
@@ -1043,7 +1044,9 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_print_format_fields(self):
|
||||
bank_amount = party_amount = total_amount = 0.0
|
||||
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
|
||||
currency = (
|
||||
bank_account_currency
|
||||
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
|
||||
party_type = None
|
||||
for d in self.get("accounts"):
|
||||
if d.party_type in ["Customer", "Supplier"] and d.party:
|
||||
|
||||
@@ -46,8 +46,10 @@ from erpnext.accounts.party import (
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_reconciliation_effect_date,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -568,7 +570,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_mandatory(self):
|
||||
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
|
||||
if not self.get(field):
|
||||
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
|
||||
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
|
||||
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
@@ -1028,7 +1030,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_doctype in get_advance_payment_doctypes():
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
@@ -1308,8 +1310,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
@@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", self.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
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
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
@@ -1699,12 +1684,15 @@ class PaymentEntry(AccountsController):
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
|
||||
@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
|
||||
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
|
||||
|
||||
def validate_entries(self):
|
||||
if not self.get("invoices"):
|
||||
@@ -826,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
today(),
|
||||
inv.difference_posting_date,
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
|
||||
@@ -829,8 +829,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
if not references:
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
|
||||
@@ -630,29 +630,58 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
def test_payment_request_on_unreconcile(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=pi.doctype,
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": pe.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Pegged Currencies", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:47:03.670913",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"pegged_currencies_item_section",
|
||||
"pegged_currency_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "pegged_currencies_item_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_currency_item",
|
||||
"fieldtype": "Table",
|
||||
"options": "Pegged Currency Details"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-02 11:46:31.936714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currencies",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencies(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pegged_currencies.pegged_currencies import PeggedCurrencies
|
||||
|
||||
pegged_currency_item: DF.Table[PeggedCurrencies]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPeggedCurrencies(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:59:28.219277",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"source_currency",
|
||||
"pegged_against",
|
||||
"pegged_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "source_currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_exchange_rate",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_against",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Pegged Against",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-17 14:11:16.521193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currency Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencyDetails(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pegged_against: DF.Link | None
|
||||
pegged_exchange_rate: DF.Data | None
|
||||
source_currency: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -5,6 +5,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
@@ -113,12 +114,22 @@
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-01 11:36:42.456429",
|
||||
"modified": "2025-07-02 17:08:04.747202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -179,8 +190,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,10 @@ class POSInvoiceMergeLog(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
||||
POSInvoiceReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consolidated_credit_note: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
customer: DF.Link
|
||||
@@ -258,6 +257,7 @@ class POSInvoiceMergeLog(Document):
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.idx = idx
|
||||
tax.row_id = None
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
@@ -583,6 +583,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.company = closing_entry.get("company") if closing_entry else None
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
|
||||
@@ -169,7 +169,7 @@ class PricingRule(Document):
|
||||
|
||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||
if tocheck and not self.get(tocheck):
|
||||
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
||||
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
|
||||
|
||||
if self.apply_rule_on_other:
|
||||
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
|
||||
|
||||
@@ -205,6 +205,56 @@ class TestPricingRule(FrappeTestCase):
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 10)
|
||||
|
||||
def test_unset_group_condition(self):
|
||||
"""
|
||||
If args are not set for group condition, then pricing rule should not be applied.
|
||||
"""
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{"item_code": "_Test Item"}],
|
||||
"currency": "USD",
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"discount_percentage": 10,
|
||||
"applicable_for": "Territory",
|
||||
"territory": "All Territories",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "_Test Currency",
|
||||
"doctype": "Sales Order",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
}
|
||||
)
|
||||
|
||||
# without territory in customer
|
||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
||||
territory = customer.territory
|
||||
|
||||
customer.territory = None
|
||||
customer.save()
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 0)
|
||||
|
||||
customer.territory = territory
|
||||
customer.save()
|
||||
|
||||
def test_pricing_rule_for_variants(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
|
||||
elif allow_blank:
|
||||
condition = f"ifnull({table}.{field}, '') = ''"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
original_gle = [
|
||||
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
|
||||
[deferred_account, 0.0, 3000, "2023-07-01"],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
|
||||
process_deferred_accounting = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
|
||||
|
||||
# cancel the process deferred accounting document
|
||||
process_deferred_accounting.cancel()
|
||||
|
||||
# check if gl entries are cancelled
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
change_acc_settings()
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
|
||||
@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("account", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("project", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
}
|
||||
},
|
||||
company: function (frm) {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -376,7 +376,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -400,7 +400,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified": "2025-07-08 16:52:12.602384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
self.validate_company_for_table("Cost Center")
|
||||
self.validate_company_for_table("Project")
|
||||
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
|
||||
if self.company != frappe.get_cached_value("Account", self.account, "company"):
|
||||
frappe.throw(
|
||||
_("Account {0} doesn't belong to Company {1}").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company_for_table(self, doctype):
|
||||
field = frappe.scrub(doctype)
|
||||
if not self.get(field):
|
||||
return
|
||||
|
||||
fieldname = field + "_name"
|
||||
|
||||
values = set(d.get(fieldname) for d in self.get(field))
|
||||
invalid_values = frappe.db.get_all(
|
||||
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
|
||||
)
|
||||
|
||||
if invalid_values:
|
||||
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
|
||||
doctype, frappe.bold(self.company)
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<ul>"
|
||||
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
|
||||
+ "</ul>"
|
||||
)
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
|
||||
@@ -1226,7 +1226,7 @@ class PurchaseInvoice(BuyingController):
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate"],
|
||||
fields=["name", "provisional_expense_account", "qty", "base_rate", "rate"],
|
||||
)
|
||||
default_provisional_account = self.get_company_default("default_provisional_account")
|
||||
provisional_accounts = set(
|
||||
@@ -1254,6 +1254,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"provisional_account": item.provisional_expense_account or default_provisional_account,
|
||||
"qty": item.qty,
|
||||
"base_rate": item.base_rate,
|
||||
"rate": item.rate,
|
||||
"has_provisional_entry": item.name in rows_with_provisional_entries,
|
||||
}
|
||||
|
||||
@@ -1270,7 +1271,10 @@ class PurchaseInvoice(BuyingController):
|
||||
self.posting_date,
|
||||
pr_item.get("provisional_account"),
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")),
|
||||
item_amount=(
|
||||
(min(item.qty, pr_item.get("qty")) * pr_item.get("rate"))
|
||||
* purchase_receipt_doc.get("conversion_rate")
|
||||
),
|
||||
)
|
||||
|
||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||
@@ -1332,17 +1336,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = (
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
)
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
|
||||
@@ -1663,7 +1663,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.posting_date = add_days(pr.posting_date, 1)
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
@@ -1672,30 +1672,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
|
||||
["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)],
|
||||
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, 1)],
|
||||
["Creditors - _TC", 0, 250, add_days(pr.posting_date, 1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 250, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pi.posting_date],
|
||||
["Provision Account - _TC", 250, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self,
|
||||
pr.name,
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel,
|
||||
pi.posting_date,
|
||||
voucher_type="Purchase Receipt",
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
@@ -1716,7 +1724,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
# Overbill PR: rate = 2000, qty = 10
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.posting_date = add_days(pr.posting_date, 1)
|
||||
pi.items[0].qty = 10
|
||||
pi.items[0].rate = 2000
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
@@ -1724,30 +1732,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
|
||||
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
|
||||
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, 1)],
|
||||
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, 1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pi.posting_date],
|
||||
["Provision Account - _TC", 5000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self,
|
||||
pr.name,
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel,
|
||||
pi.posting_date,
|
||||
voucher_type="Purchase Receipt",
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
@@ -1780,13 +1796,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 1000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 1000, pi.posting_date],
|
||||
["Provision Account - _TC", 1000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
def test_provisional_accounting_entry_multi_currency(self):
|
||||
setup_provisional_accounting()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test Non Stock Item",
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
qty=1000,
|
||||
rate=111.11,
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
supplier="_Test Supplier USD",
|
||||
)
|
||||
pr.conversion_rate = 0.014783000
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, 1)
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
self.assertEqual(pr.items[0].provisional_expense_account, "Provision Account - _TC")
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
["_Test Payable USD - _TC", 0, 1642.54, add_days(pr.posting_date, 1)],
|
||||
["Cost of Goods Sold - _TC", 1642.54, 0, add_days(pr.posting_date, 1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 1642.54, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 1642.54, pi.posting_date],
|
||||
["Provision Account - _TC", 1642.54, 0, pi.posting_date],
|
||||
]
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pi.posting_date],
|
||||
["Provision Account - _TC", 0, 1642.54, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(
|
||||
self,
|
||||
pr.name,
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel,
|
||||
pi.posting_date,
|
||||
voucher_type="Purchase Receipt",
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
|
||||
@@ -2024,6 +2024,7 @@
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -2188,7 +2189,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-17 19:32:31.809658",
|
||||
"modified": "2025-06-26 14:06:56.773552",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -1356,7 +1356,9 @@ class SalesInvoice(SellingController):
|
||||
if item.is_fixed_asset:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
if self.is_return:
|
||||
if (self.docstatus == 2 and not self.is_return) or (
|
||||
self.docstatus == 1 and self.is_return
|
||||
):
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
@@ -1369,8 +1371,10 @@ class SalesInvoice(SellingController):
|
||||
add_asset_activity(asset.name, _("Asset returned"))
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
posting_date = frappe.db.get_value(
|
||||
"Sales Invoice", self.return_against, "posting_date"
|
||||
posting_date = (
|
||||
frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
if self.is_return
|
||||
else self.posting_date
|
||||
)
|
||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||
notes = _(
|
||||
@@ -1467,8 +1471,10 @@ class SalesInvoice(SellingController):
|
||||
return self._enable_discount_accounting
|
||||
|
||||
def set_asset_status(self, asset):
|
||||
if self.is_return:
|
||||
if self.is_return and not self.docstatus == 2:
|
||||
asset.set_status()
|
||||
elif self.is_return and self.docstatus == 2:
|
||||
asset.set_status("Sold")
|
||||
else:
|
||||
asset.set_status("Sold" if self.docstatus == 1 else None)
|
||||
|
||||
|
||||
@@ -836,6 +836,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
w = self.make()
|
||||
self.assertEqual(w.outstanding_amount, w.base_rounded_total)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0},
|
||||
)
|
||||
def test_rounded_total_with_cash_discount(self):
|
||||
si = frappe.copy_doc(test_records[2])
|
||||
|
||||
@@ -2493,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2547,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2554,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
@@ -2597,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Days"
|
||||
acc_settings.save()
|
||||
|
||||
def test_validate_inter_company_transaction_address_links(self):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
@@ -2849,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.items[0].rate, rate)
|
||||
self.assertEqual(target_doc.items[0].rate, rate)
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
check_gl_entries(
|
||||
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
|
||||
)
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
@@ -3135,6 +3141,65 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
|
||||
|
||||
def test_depreciation_on_cancel_invoice(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
create_asset_data()
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
purchase_date="2020-01-01",
|
||||
available_for_use_date="2023-01-01",
|
||||
depreciation_start_date="2023-04-01",
|
||||
calculate_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
post_depreciation_entries()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="Macbook Pro", asset=asset.name, qty=1, rate=10000, posting_date=getdate("2025-05-01")
|
||||
)
|
||||
return_si = make_return_doc("Sales Invoice", si.name)
|
||||
return_si.posting_date = getdate("2025-05-01")
|
||||
return_si.submit()
|
||||
return_si.reload()
|
||||
return_si.cancel()
|
||||
|
||||
asset.load_from_db()
|
||||
|
||||
# Check if the asset schedule is updated while cancel the return invoice
|
||||
expected_values = [
|
||||
["2023-04-01", 4986.30, 4986.30, True],
|
||||
["2024-04-01", 20000.0, 24986.30, True],
|
||||
["2025-04-01", 20000.0, 44986.30, True],
|
||||
["2025-05-01", 1643.84, 46630.14, True],
|
||||
]
|
||||
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
|
||||
|
||||
si.reload()
|
||||
si.cancel()
|
||||
asset.load_from_db()
|
||||
|
||||
# Check if the asset schedule is updated while cancel the sales invoice
|
||||
expected_values = [
|
||||
["2023-04-01", 4986.30, 4986.30, True],
|
||||
["2024-04-01", 20000.0, 24986.30, True],
|
||||
["2025-04-01", 20000.0, 44986.30, True],
|
||||
["2026-04-01", 20000.0, 64986.30, False],
|
||||
["2027-04-01", 20000.0, 84986.30, False],
|
||||
["2028-01-01", 15013.70, 100000.0, False],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
|
||||
|
||||
def test_sales_invoice_against_supplier(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
@@ -3429,6 +3494,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3436,24 +3502,23 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
|
||||
|
||||
dn = create_delivery_note()
|
||||
dn.submit()
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
# make a copy of first item and add it to invoice
|
||||
item_copy = frappe.copy_doc(si.items[0])
|
||||
si.save()
|
||||
|
||||
si.items = [] # Clear existing items
|
||||
si.append("items", item_copy)
|
||||
si.save()
|
||||
|
||||
si.append("items", item_copy)
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
|
||||
dn.cancel()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -4409,6 +4474,94 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||
|
||||
def test_stand_alone_credit_note_valuation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = "_Test Item for Credit Note Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-TCNV.####",
|
||||
},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=-2,
|
||||
rate=1200,
|
||||
is_return=1,
|
||||
update_stock=1,
|
||||
)
|
||||
|
||||
stock_ledger_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 1200.0)
|
||||
self.assertEqual(stock_ledger_entry.valuation_rate, 1200.0)
|
||||
self.assertEqual(stock_ledger_entry.qty, 2.0)
|
||||
self.assertEqual(stock_ledger_entry.stock_value_difference, 2400.0)
|
||||
|
||||
def test_stand_alone_credit_note_zero_valuation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = "_Test Item for Credit Note Zero Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-TCNZV.####",
|
||||
},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=-2,
|
||||
rate=1200,
|
||||
is_return=1,
|
||||
update_stock=1,
|
||||
allow_zero_valuation_rate=1,
|
||||
)
|
||||
|
||||
stock_ledger_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
||||
self.assertEqual(stock_ledger_entry.valuation_rate, 0.0)
|
||||
self.assertEqual(stock_ledger_entry.qty, 2.0)
|
||||
self.assertEqual(stock_ledger_entry.stock_value_difference, 0.0)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item(item_code, properties=properties)
|
||||
item.is_stock_item = 1
|
||||
item.save()
|
||||
return item
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
@@ -4436,6 +4589,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
|
||||
)
|
||||
gl_entries = q.run(as_dict=True)
|
||||
|
||||
doc.assertGreater(len(gl_entries), 0)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
@@ -4512,6 +4667,7 @@ def create_sales_invoice(**args):
|
||||
"conversion_factor": args.get("conversion_factor", 1),
|
||||
"incoming_rate": args.incoming_rate or 0,
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_advance_payment_doctypes,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
@@ -84,7 +85,7 @@ class UnreconcilePayment(Document):
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if doc.doctype in get_advance_payment_doctypes():
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
@@ -692,7 +692,18 @@ def make_reverse_gl_entries(
|
||||
query.run()
|
||||
else:
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
gle_names = [x.get("name") for x in gl_entries]
|
||||
|
||||
# if names are available, cancel only that set of entries
|
||||
if not all(gle_names):
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
|
||||
modified=%s, modified_by=%s
|
||||
where name in %s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, tuple(gle_names)),
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
|
||||
@@ -424,6 +424,8 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
|
||||
Will first search in party (Customer / Supplier) record, if not found,
|
||||
will search in group (Customer Group / Supplier Group),
|
||||
finally will return default."""
|
||||
if not party_type:
|
||||
frappe.throw(_("Party Type is mandatory"))
|
||||
if not company:
|
||||
frappe.throw(_("Please select a Company"))
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes,
|
||||
get_currency_precision,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -49,7 +53,8 @@ class ReceivablePayableReport:
|
||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||
self.age_as_on = (
|
||||
getdate(nowdate())
|
||||
if self.filters.calculate_ageing_with == "Today Date"
|
||||
if "calculate_ageing_with" not in self.filters
|
||||
or self.filters.calculate_ageing_with == "Today Date"
|
||||
else self.filters.report_date
|
||||
)
|
||||
|
||||
@@ -61,6 +66,7 @@ class ReceivablePayableReport:
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -84,6 +90,7 @@ class ReceivablePayableReport:
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.previous_party = ""
|
||||
@@ -180,7 +187,10 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or (
|
||||
ple.voucher_type in ("Payment Entry", "Journal Entry")
|
||||
and ple.against_voucher_type in self.advance_payment_doctypes
|
||||
):
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -79,6 +79,14 @@ class Deferred_Item:
|
||||
return - estimated amount to post for given period
|
||||
Calculated based on already booked amount and item service period
|
||||
"""
|
||||
if self.filters.book_deferred_entries_based_on == "Months":
|
||||
# if the deferred entries are based on service period, use service start and end date
|
||||
return self.calculate_monthly_amount(start_date, end_date)
|
||||
|
||||
else:
|
||||
return self.calculate_days_amount(start_date, end_date)
|
||||
|
||||
def calculate_monthly_amount(self, start_date, end_date):
|
||||
total_months = (
|
||||
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||
+ (self.service_end_date.month - self.service_start_date.month)
|
||||
@@ -105,6 +113,19 @@ class Deferred_Item:
|
||||
|
||||
return base_amount
|
||||
|
||||
def calculate_days_amount(self, start_date, end_date):
|
||||
base_amount = 0
|
||||
total_days = date_diff(self.service_end_date, self.service_start_date) + 1
|
||||
total_booking_days = date_diff(end_date, start_date) + 1
|
||||
already_booked_amount = self.get_item_total()
|
||||
|
||||
base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days))
|
||||
|
||||
if base_amount + already_booked_amount > self.base_net_amount:
|
||||
base_amount = self.base_net_amount - already_booked_amount
|
||||
|
||||
return base_amount
|
||||
|
||||
def make_dummy_gle(self, name, date, amount):
|
||||
"""
|
||||
return - frappe._dict() of a dummy gle entry
|
||||
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
else:
|
||||
self.filters = frappe._dict(filters)
|
||||
|
||||
self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
|
||||
self.period_list = None
|
||||
self.deferred_invoices = []
|
||||
# holds period wise total for report
|
||||
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.on(
|
||||
(inv_item.name == gle.voucher_detail_no)
|
||||
& (deferred_account_field == gle.account)
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -109,14 +109,19 @@ def get_period_list(
|
||||
|
||||
|
||||
def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
|
||||
fiscal_year = frappe.db.sql(
|
||||
"""select min(year_start_date) as year_start_date,
|
||||
max(year_end_date) as year_end_date from `tabFiscal Year` where
|
||||
name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
|
||||
{"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
|
||||
as_dict=1,
|
||||
from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date")
|
||||
to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date")
|
||||
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date"))
|
||||
.where(fy.year_start_date >= from_year_start_date)
|
||||
.where(fy.year_end_date <= to_year_end_date)
|
||||
)
|
||||
|
||||
fiscal_year = query.run(as_dict=True)
|
||||
return fiscal_year[0] if fiscal_year else {}
|
||||
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_err",
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
|
||||
get_customer_details,
|
||||
)
|
||||
@@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si.is_internal_customer,
|
||||
si.customer,
|
||||
si.remarks,
|
||||
si.territory,
|
||||
fn.IfNull(si.territory, "Not Specified").as_("territory"),
|
||||
si.company,
|
||||
si.base_net_total,
|
||||
sii.project,
|
||||
@@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.base_net_rate,
|
||||
sii.base_net_amount,
|
||||
si.customer_name,
|
||||
si.customer_group,
|
||||
fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
|
||||
sii.so_detail,
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
|
||||
@@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": add_to_date(mid_year, days=1),
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = "TDS - 3"
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(
|
||||
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
|
||||
)
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
|
||||
inv_1.apply_tds = 1
|
||||
inv_2.tax_withholding_category = "TDS - 3"
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
if doc:
|
||||
doc.fiscal_year = years[0]
|
||||
else:
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -629,6 +629,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
update_advance_paid = []
|
||||
|
||||
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
@@ -713,23 +714,8 @@ def update_reference_in_payment_entry(
|
||||
update_advance_paid = []
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", payment_entry.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
|
||||
|
||||
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
if d.voucher_detail_no:
|
||||
@@ -783,6 +769,28 @@ def update_reference_in_payment_entry(
|
||||
return row, update_advance_paid
|
||||
|
||||
|
||||
def get_reconciliation_effect_date(reference, company, posting_date):
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(
|
||||
reference.against_voucher_type, reference.against_voucher, date_field
|
||||
)
|
||||
if getdate(reconcile_on) < getdate(posting_date):
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
return reconcile_on
|
||||
|
||||
|
||||
def cancel_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
|
||||
) -> None:
|
||||
@@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe(
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
pay = qb.DocType("Payment Entry")
|
||||
|
||||
linked_pe = (
|
||||
query = (
|
||||
qb.from_(per)
|
||||
.select(per.parent)
|
||||
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)))
|
||||
.run(as_list=1)
|
||||
)
|
||||
linked_pe = convert_to_list(linked_pe)
|
||||
# remove reference only from specified payment
|
||||
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
|
||||
|
||||
if linked_pe:
|
||||
update_query = (
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
|
||||
.select("*")
|
||||
.where(
|
||||
(per.reference_doctype == ref_type)
|
||||
& (per.reference_name == ref_no)
|
||||
& (per.docstatus.lt(2))
|
||||
& (per.parenttype == "Payment Entry")
|
||||
)
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
update_query = update_query.where(per.parent == payment_name)
|
||||
# update reference only from specified payment
|
||||
if payment_name:
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
update_query.run()
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [
|
||||
x
|
||||
for x in pe_doc.references
|
||||
if x.reference_doctype == ref_type and x.reference_name == ref_no
|
||||
]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
linked_pe = set()
|
||||
row_names = set()
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
for row in reference_rows:
|
||||
linked_pe.add(row.parent)
|
||||
row_names.add(row.name)
|
||||
|
||||
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
|
||||
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
|
||||
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
|
||||
pay.modified_by, frappe.session.user
|
||||
).where(pay.name == pe).run()
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
# Update payment request amount
|
||||
update_payment_requests_as_per_pe_references(reference_rows, cancel=True)
|
||||
|
||||
# Update allocated amounts and modified fields in one go
|
||||
(
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.name.isin(row_names))
|
||||
.where(per.parenttype == "Payment Entry")
|
||||
.run()
|
||||
)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [x for x in pe_doc.references if x.name in row_names]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
|
||||
(
|
||||
qb.update(pay)
|
||||
.set(pay.total_allocated_amount, pe_doc.total_allocated_amount)
|
||||
.set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount)
|
||||
.set(pay.unallocated_amount, pe_doc.unallocated_amount)
|
||||
.set(pay.modified, now())
|
||||
.set(pay.modified_by, frappe.session.user)
|
||||
.where(pay.name == pe)
|
||||
.run()
|
||||
)
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -2235,6 +2264,15 @@ def get_party_types_from_account_type(account_type):
|
||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||
|
||||
|
||||
def get_advance_payment_doctypes():
|
||||
"""
|
||||
Get list of advance payment doctypes based on type.
|
||||
:param type: Optional, can be "receivable" or "payable". If not provided, returns both.
|
||||
"""
|
||||
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
|
||||
def run_ledger_health_checks():
|
||||
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
if health_monitor_settings.enable_health_monitor:
|
||||
|
||||
@@ -72,6 +72,12 @@ frappe.ui.form.on("Asset", {
|
||||
filters: { item_code: doc.item_code },
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
frm.custom_make_buttons = {
|
||||
"Asset Capitalization": "Asset Capitalization",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -62,8 +62,8 @@ frappe.ui.form.on("Asset Movement", {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 0, reqd: 0 },
|
||||
to_employee: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 0 },
|
||||
};
|
||||
} else if (frm.doc.purpose === "Issue") {
|
||||
fieldnames_to_be_altered = {
|
||||
@@ -72,6 +72,13 @@ frappe.ui.form.on("Asset Movement", {
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 1 },
|
||||
};
|
||||
} else if (frm.doc.purpose === "Transfer and Issue") {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 0, reqd: 1 },
|
||||
from_employee: { read_only: 0, reqd: 1 },
|
||||
to_employee: { read_only: 0, reqd: 1 },
|
||||
};
|
||||
}
|
||||
if (fieldnames_to_be_altered) {
|
||||
Object.keys(fieldnames_to_be_altered).forEach((fieldname) => {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"fieldname": "purpose",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purpose",
|
||||
"options": "\nIssue\nReceipt\nTransfer",
|
||||
"options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -93,10 +93,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-28 16:54:26.571083",
|
||||
"modified": "2025-05-30 17:01:55.864353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Movement",
|
||||
@@ -149,7 +150,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,105 +24,81 @@ class AssetMovement(Document):
|
||||
amended_from: DF.Link | None
|
||||
assets: DF.Table[AssetMovementItem]
|
||||
company: DF.Link
|
||||
purpose: DF.Literal["", "Issue", "Receipt", "Transfer"]
|
||||
purpose: DF.Literal["", "Issue", "Receipt", "Transfer", "Transfer and Issue"]
|
||||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
transaction_date: DF.Datetime
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_asset()
|
||||
self.validate_location()
|
||||
self.validate_employee()
|
||||
|
||||
def validate_asset(self):
|
||||
for d in self.assets:
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
|
||||
frappe.throw(_("{0} asset cannot be transferred").format(status))
|
||||
self.validate_asset(d)
|
||||
self.validate_movement(d)
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
|
||||
def validate_asset(self, d):
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
|
||||
frappe.throw(_("{0} asset cannot be transferred").format(status))
|
||||
|
||||
if not (d.source_location or d.target_location or d.from_employee or d.to_employee):
|
||||
frappe.throw(_("Either location or employee must be required"))
|
||||
if company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
|
||||
|
||||
def validate_location(self):
|
||||
for d in self.assets:
|
||||
if self.purpose in ["Transfer", "Issue"]:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the location {1}").format(
|
||||
d.asset, d.source_location
|
||||
)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
def validate_movement(self, d):
|
||||
if self.purpose == "Transfer and Issue":
|
||||
self.validate_location_and_employee(d)
|
||||
elif self.purpose in ["Receipt", "Transfer"]:
|
||||
self.validate_location(d)
|
||||
else:
|
||||
self.validate_employee(d)
|
||||
|
||||
if self.purpose == "Issue":
|
||||
if d.target_location:
|
||||
def validate_location_and_employee(self, d):
|
||||
self.validate_location(d)
|
||||
self.validate_employee(d)
|
||||
|
||||
def validate_location(self, d):
|
||||
if self.purpose in ["Transfer", "Transfer and Issue"]:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
if not d.to_employee:
|
||||
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if self.purpose == "Transfer":
|
||||
if d.to_employee:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
if not d.target_location:
|
||||
frappe.throw(
|
||||
_("Target Location is required while transferring Asset {0}").format(d.asset)
|
||||
)
|
||||
if d.source_location == d.target_location:
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not (d.source_location) and not (d.target_location or d.to_employee):
|
||||
frappe.throw(
|
||||
_("Target Location or To Employee is required while receiving Asset {0}").format(
|
||||
d.asset
|
||||
)
|
||||
)
|
||||
elif d.source_location:
|
||||
if d.from_employee and not d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Target Location is required while receiving Asset {0} from an employee"
|
||||
).format(d.asset)
|
||||
)
|
||||
elif d.to_employee and d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} cannot be received at a location and given to an employee in a single movement"
|
||||
).format(d.asset)
|
||||
)
|
||||
|
||||
def validate_employee(self):
|
||||
for d in self.assets:
|
||||
if d.from_employee:
|
||||
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
if not d.target_location:
|
||||
frappe.throw(_("Target Location is required for transferring Asset {0}").format(d.asset))
|
||||
if d.source_location == d.target_location:
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not d.target_location:
|
||||
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def validate_employee(self, d):
|
||||
if self.purpose == "Tranfer and Issue":
|
||||
if not d.from_employee:
|
||||
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if d.from_employee:
|
||||
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
)
|
||||
|
||||
if not d.to_employee:
|
||||
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
@@ -130,53 +106,63 @@ class AssetMovement(Document):
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def set_latest_location_and_custodian_in_asset(self):
|
||||
for d in self.assets:
|
||||
current_location, current_employee = self.get_latest_location_and_custodian(d.asset)
|
||||
self.update_asset_location_and_custodian(d.asset, current_location, current_employee)
|
||||
self.log_asset_activity(d.asset, current_location, current_employee)
|
||||
|
||||
def get_latest_location_and_custodian(self, asset):
|
||||
current_location, current_employee = "", ""
|
||||
cond = "1=1"
|
||||
|
||||
for d in self.assets:
|
||||
args = {"asset": d.asset, "company": self.company}
|
||||
# latest entry corresponds to current document's location, employee when transaction date > previous dates
|
||||
# In case of cancellation it corresponds to previous latest document's location, employee
|
||||
args = {"asset": asset, "company": self.company}
|
||||
latest_movement_entry = frappe.db.sql(
|
||||
f"""
|
||||
SELECT asm_item.target_location, asm_item.to_employee
|
||||
FROM `tabAsset Movement Item` asm_item
|
||||
JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name
|
||||
WHERE
|
||||
asm_item.asset = %(asset)s AND
|
||||
asm.company = %(company)s AND
|
||||
asm.docstatus = 1 AND {cond}
|
||||
ORDER BY asm.transaction_date DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
args,
|
||||
)
|
||||
|
||||
# latest entry corresponds to current document's location, employee when transaction date > previous dates
|
||||
# In case of cancellation it corresponds to previous latest document's location, employee
|
||||
latest_movement_entry = frappe.db.sql(
|
||||
f"""
|
||||
SELECT asm_item.target_location, asm_item.to_employee
|
||||
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm
|
||||
WHERE
|
||||
asm_item.parent=asm.name and
|
||||
asm_item.asset=%(asset)s and
|
||||
asm.company=%(company)s and
|
||||
asm.docstatus=1 and {cond}
|
||||
ORDER BY
|
||||
asm.transaction_date desc limit 1
|
||||
""",
|
||||
args,
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
return current_location, current_employee
|
||||
|
||||
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
||||
asset = frappe.get_doc("Asset", asset_id)
|
||||
|
||||
if employee and employee != asset.custodian:
|
||||
frappe.db.set_value("Asset", asset_id, "custodian", employee)
|
||||
if location and location != asset.location:
|
||||
frappe.db.set_value("Asset", asset_id, "location", location)
|
||||
|
||||
def log_asset_activity(self, asset_id, location, employee):
|
||||
if location and employee:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", location),
|
||||
get_link_to_form("Employee", employee),
|
||||
),
|
||||
)
|
||||
elif location:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset transferred to Location {0}").format(get_link_to_form("Location", location)),
|
||||
)
|
||||
elif employee:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", employee)),
|
||||
)
|
||||
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
|
||||
frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
|
||||
|
||||
if current_location and current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", current_location),
|
||||
get_link_to_form("Employee", current_employee),
|
||||
),
|
||||
)
|
||||
elif current_location:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset transferred to Location {0}").format(
|
||||
get_link_to_form("Location", current_location)
|
||||
),
|
||||
)
|
||||
elif current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
)
|
||||
|
||||
# after issuing, asset should belong to an employee not at a location
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
|
||||
|
||||
create_asset_movement(
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.ui.form.on("Asset Repair", {
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frm.fields_dict.cost_center.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -167,4 +169,37 @@ frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
var row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate);
|
||||
},
|
||||
|
||||
pick_serial_and_batch(frm, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let doc = frm.doc;
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.qty = item.consumed_quantity;
|
||||
item.type_of_transaction = item.consumed_quantity > 0 ? "Outward" : "Inward";
|
||||
|
||||
item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
frm.doc.posting_date = frappe.datetime.get_today();
|
||||
frm.doc.posting_time = frappe.datetime.now_time();
|
||||
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
valuation_rate: r.avg_rate,
|
||||
consumed_quantity: Math.abs(r.total_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -248,7 +249,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 16:14:14.398356",
|
||||
"modified": "2025-06-29 22:30:00.589597",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@@ -292,4 +293,4 @@
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class AssetRepair(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
self.validate_asset()
|
||||
self.validate_dates()
|
||||
self.update_status()
|
||||
|
||||
@@ -60,6 +61,14 @@ class AssetRepair(AccountsController):
|
||||
self.set_stock_items_cost()
|
||||
self.calculate_total_repair_cost()
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
frappe.throw(
|
||||
_("Asset {0} is in {1} status and cannot be repaired.").format(
|
||||
get_link_to_form("Asset", self.asset), self.asset_doc.status
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
frappe.throw(
|
||||
@@ -131,6 +140,13 @@ class AssetRepair(AccountsController):
|
||||
),
|
||||
)
|
||||
|
||||
def cancel_sabb(self):
|
||||
for row in self.stock_items:
|
||||
if sabb := row.serial_and_batch_bundle:
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sabb)
|
||||
doc.cancel()
|
||||
|
||||
def before_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
@@ -172,6 +188,8 @@ class AssetRepair(AccountsController):
|
||||
),
|
||||
)
|
||||
|
||||
self.cancel_sabb()
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
@@ -33,6 +34,33 @@ class TestAssetRepair(unittest.TestCase):
|
||||
create_item("_Test Stock Item")
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_asset_status(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"company": "_Test Company", "asset": asset.name, "asset_name": asset.asset_name})
|
||||
self.assertRaises(frappe.ValidationError, asset_repair.save)
|
||||
|
||||
def test_update_status(self):
|
||||
asset = create_asset(submit=1)
|
||||
initial_status = asset.status
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"consumed_quantity",
|
||||
"total_value",
|
||||
"serial_no",
|
||||
"column_break_xzfr",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle"
|
||||
],
|
||||
"fields": [
|
||||
@@ -61,12 +63,21 @@
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xzfr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 12:01:47.147333",
|
||||
"modified": "2025-06-27 14:52:56.311166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
@@ -76,4 +87,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,10 @@ class AssetValueAdjustment(Document):
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
salvage_value_adjustment = (
|
||||
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
|
||||
)
|
||||
row.expected_value_after_useful_life += salvage_value_adjustment
|
||||
row.value_after_depreciation += flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
@@ -206,6 +210,12 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.save()
|
||||
asset.set_status()
|
||||
|
||||
def get_adjusted_salvage_value_amount(self, row, difference_amount):
|
||||
if row.expected_value_after_useful_life:
|
||||
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100
|
||||
return flt(salvage_value_adjustment if self.docstatus == 1 else -1 * salvage_value_adjustment)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -292,6 +292,43 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
|
||||
|
||||
def test_expected_value_after_useful_life(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 5000,
|
||||
"salvage_value_percentage": 5,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 5000.0)
|
||||
|
||||
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=current_asset_value,
|
||||
new_asset_value=40000,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||
self.assertEqual(difference_amount, -60000)
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 2000.0)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -807,11 +807,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.amount = flt(obj.amount) - flt(obj.billed_amt)
|
||||
target.base_amount = target.amount * flt(source_parent.conversion_rate)
|
||||
target.qty = (
|
||||
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty)
|
||||
)
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
|
||||
@@ -1286,6 +1286,25 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertFalse(po.per_billed)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
@change_settings("Buying Settings", {"maintain_same_rate": 0})
|
||||
def test_purchase_invoice_creation_with_partial_qty(self):
|
||||
po = create_purchase_order(qty=100, rate=10)
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.items[0].qty = 42
|
||||
pi.items[0].rate = 7.5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 58)
|
||||
self.assertEqual(pi.items[0].rate, 10)
|
||||
pi.items[0].qty = 8
|
||||
pi.items[0].rate = 5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 50)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters):
|
||||
|
||||
datapoints = []
|
||||
|
||||
start = 2 if filters.get("based_on") in ["Item", "Supplier"] else 1
|
||||
if filters.get("based_on") in ["Supplier"]:
|
||||
start = 3
|
||||
elif filters.get("based_on") in ["Item"]:
|
||||
start = 2
|
||||
else:
|
||||
start = 1
|
||||
|
||||
if filters.get("group_by"):
|
||||
start += 1
|
||||
|
||||
# fetch only periodic columns as labels
|
||||
columns = conditions.get("columns")[start:-2][1::2]
|
||||
columns = conditions.get("columns")[start:-2][2::2]
|
||||
labels = [column.split(":")[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start - 1]:
|
||||
if not row[start]:
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
row = row[1::2]
|
||||
row = row[2::2]
|
||||
|
||||
for i in range(len(row)):
|
||||
datapoints[i] += row[i]
|
||||
|
||||
@@ -51,6 +51,9 @@ from erpnext.accounts.utils import (
|
||||
get_fiscal_years,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes as _get_advance_payment_doctypes,
|
||||
)
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.controllers.print_settings import (
|
||||
set_print_templates_for_item_table,
|
||||
@@ -386,9 +389,7 @@ class AccountsController(TransactionBase):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.doctype in advance_payment_doctypes:
|
||||
if self.doctype in self.get_advance_payment_doctypes():
|
||||
qb.from_(adv).delete().where(
|
||||
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
|
||||
).run()
|
||||
@@ -1134,10 +1135,21 @@ class AccountsController(TransactionBase):
|
||||
return True
|
||||
|
||||
def set_taxes_and_charges(self):
|
||||
if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"):
|
||||
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
if self.doctype == "Material Request":
|
||||
# Material Request does not have taxes
|
||||
return
|
||||
|
||||
if self.get("taxes") or self.get("is_pos"):
|
||||
return
|
||||
|
||||
if frappe.get_single_value(
|
||||
"Accounts Settings", "add_taxes_from_taxes_and_charges_template"
|
||||
) and hasattr(self, "taxes_and_charges"):
|
||||
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
self.append_taxes_from_item_tax_template()
|
||||
|
||||
def append_taxes_from_master(self, tax_master_doctype=None):
|
||||
if self.get("taxes_and_charges"):
|
||||
@@ -1169,6 +1181,9 @@ class AccountsController(TransactionBase):
|
||||
"account_head": account_head,
|
||||
"rate": 0,
|
||||
"description": account_head,
|
||||
"set_by_item_tax_template": 1,
|
||||
"category": "Total",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2053,69 +2068,48 @@ class AccountsController(TransactionBase):
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
||||
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
if not ref_wise_billed_amount:
|
||||
return
|
||||
|
||||
total_overbilled_amt = 0.0
|
||||
overbilled_items = []
|
||||
precision = self.precision(based_on, "items")
|
||||
precision_allowance = 1 / (10**precision)
|
||||
|
||||
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
|
||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.get(item_ref_dn):
|
||||
continue
|
||||
for row in ref_wise_billed_amount.values():
|
||||
total_billed_amt = row.billed_amt
|
||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
)
|
||||
|
||||
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||
|
||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
||||
total_billed_amt = abs(total_billed_amt)
|
||||
max_allowed_amt = abs(max_allowed_amt)
|
||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||
|
||||
overbill_amt = total_billed_amt - max_allowed_amt
|
||||
row["max_allowed_amt"] = max_allowed_amt
|
||||
total_overbilled_amt += overbill_amt
|
||||
|
||||
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(
|
||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||
if self.doctype != "Purchase Invoice" or not cint(
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
overbilled_items.append(row)
|
||||
|
||||
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||
if overbilled_items:
|
||||
self.throw_overbill_exception(overbilled_items, precision)
|
||||
|
||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(
|
||||
_("Overbilling of {} ignored because you have {} role.").format(
|
||||
total_overbilled_amt, role_allowed_to_over_bill
|
||||
total_overbilled_amt, role_allowed_to_overbill
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
@@ -2131,55 +2125,88 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
||||
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
|
||||
"""
|
||||
Returns Sum of Amount of
|
||||
Sales/Purchase Invoice Items
|
||||
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
|
||||
that are submitted OR not submitted but are under current invoice
|
||||
"""
|
||||
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
|
||||
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
if not reference_names:
|
||||
return
|
||||
|
||||
item_doctype = frappe.qb.DocType(item.doctype)
|
||||
ref_wise_billed_amount = {}
|
||||
precision = self.precision(based_on, "items")
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
||||
|
||||
for item in self.items:
|
||||
key = item.get(item_ref_dn)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(key), precision)
|
||||
current_amount = flt(item.get(based_on), precision)
|
||||
|
||||
if not ref_amt:
|
||||
if current_amount: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
ref_wise_billed_amount.setdefault(
|
||||
key,
|
||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||
)
|
||||
|
||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||
if key in already_billed:
|
||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||
|
||||
return ref_wise_billed_amount
|
||||
|
||||
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
|
||||
item_doctype = frappe.qb.DocType(self.items[0].doctype)
|
||||
based_on_field = frappe.qb.Field(based_on)
|
||||
join_field = frappe.qb.Field(item_ref_dn)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(Sum(based_on_field))
|
||||
.where(join_field == item.get(item_ref_dn))
|
||||
.where(
|
||||
Criterion.any(
|
||||
[ # select all items from other invoices OR current invoices
|
||||
Criterion.all(
|
||||
[ # for selecting items from other invoices
|
||||
item_doctype.docstatus == 1,
|
||||
item_doctype.parent != self.name,
|
||||
]
|
||||
),
|
||||
Criterion.all(
|
||||
[ # for selecting items from current invoice, that are linked to same reference
|
||||
item_doctype.docstatus == 0,
|
||||
item_doctype.parent == self.name,
|
||||
item_doctype.name != item.name,
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
return result[0][0] if result else 0
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
|
||||
).format(item.item_code, item.idx, max_allowed_amt)
|
||||
return frappe._dict(
|
||||
(
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(join_field, Sum(based_on_field))
|
||||
.where(join_field.isin(reference_names))
|
||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
|
||||
.groupby(join_field)
|
||||
).run()
|
||||
)
|
||||
|
||||
def throw_overbill_exception(self, overbilled_items, precision):
|
||||
message = (
|
||||
_("<p>Cannot overbill for the following Items:</p>")
|
||||
+ "<ul>"
|
||||
+ "".join(
|
||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||
frappe.bold(item.item_code),
|
||||
", ".join(str(x) for x in item.rows),
|
||||
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
|
||||
)
|
||||
for item in overbilled_items
|
||||
)
|
||||
+ "</ul>"
|
||||
)
|
||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||
|
||||
frappe.throw(_(message))
|
||||
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
||||
@@ -2886,7 +2913,7 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger.submit()
|
||||
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
return _get_advance_payment_doctypes()
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
@@ -3939,6 +3966,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
|
||||
@@ -394,7 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
page_len = 30
|
||||
page_len = 300
|
||||
|
||||
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))
|
||||
|
||||
@@ -680,6 +680,14 @@ def get_rate_for_return(
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
if not rate and voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
details = frappe.db.get_value(
|
||||
voucher_type + " Item", voucher_detail_no, ["rate", "allow_zero_valuation_rate"], as_dict=1
|
||||
)
|
||||
|
||||
if details and not details.allow_zero_valuation_rate:
|
||||
rate = flt(details.rate)
|
||||
|
||||
return rate
|
||||
|
||||
|
||||
|
||||
@@ -525,6 +525,15 @@ class SellingController(StockController):
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
if (
|
||||
self.get("is_return")
|
||||
and not d.incoming_rate
|
||||
and not self.get("return_against")
|
||||
and not self.is_internal_transfer()
|
||||
and not d.get("allow_zero_valuation_rate")
|
||||
):
|
||||
d.incoming_rate = d.rate
|
||||
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
if self.doctype == "Delivery Note" or self.get("update_stock"):
|
||||
|
||||
@@ -156,6 +156,17 @@ status_map = {
|
||||
["Draft", None],
|
||||
["Completed", "eval:self.docstatus == 1"],
|
||||
],
|
||||
"Pick List": [
|
||||
["Draft", None],
|
||||
["Open", "eval:self.docstatus == 1"],
|
||||
["Completed", "stock_entry_exists"],
|
||||
[
|
||||
"Partly Delivered",
|
||||
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
|
||||
],
|
||||
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -221,7 +221,11 @@ class StockController(AccountsController):
|
||||
parent_details = self.get_parent_details_for_packed_items()
|
||||
|
||||
for row in self.get(table_name):
|
||||
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
|
||||
if (
|
||||
not via_landed_cost_voucher
|
||||
and row.serial_and_batch_bundle
|
||||
and (row.serial_no or row.batch_no)
|
||||
):
|
||||
self.validate_serial_nos_and_batches_with_bundle(row)
|
||||
|
||||
if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
|
||||
@@ -909,7 +913,7 @@ class StockController(AccountsController):
|
||||
fieldname = f"{dimension.source_fieldname}"
|
||||
|
||||
sl_dict[dimension.target_fieldname] = row.get(fieldname)
|
||||
return
|
||||
continue
|
||||
|
||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||
else:
|
||||
@@ -1644,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items):
|
||||
"sample_size": flt(item.get("sample_size")),
|
||||
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||
"batch_no": item.get("batch_no"),
|
||||
"child_row_reference": item.get("child_row_reference"),
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
quality_inspection.save()
|
||||
inspections.append(quality_inspection.name)
|
||||
|
||||
@@ -1658,14 +1663,9 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -601,12 +601,15 @@ class SubcontractingController(StockController):
|
||||
rm_obj.use_serial_batch_fields = 1
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
if not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
elif self.backflush_based_on == "BOM":
|
||||
self.update_rate_for_supplied_items()
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
|
||||
@@ -931,7 +931,10 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
@change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1},
|
||||
)
|
||||
def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
|
||||
# Create a Sales Taxes and Charges Template
|
||||
if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
|
||||
@@ -960,6 +963,30 @@ class TestAccountsController(FrappeTestCase):
|
||||
|
||||
self.assertEqual(sinv.total_taxes_and_charges, 4.5)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0},
|
||||
)
|
||||
def test_19_fetch_taxes_based_on_item_tax_template_template(self):
|
||||
# Create a Sales Invoice
|
||||
sinv = frappe.new_doc("Sales Invoice")
|
||||
sinv.customer = self.customer
|
||||
sinv.company = self.company
|
||||
sinv.currency = "INR"
|
||||
sinv.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 50,
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
},
|
||||
)
|
||||
sinv.insert()
|
||||
|
||||
self.assertEqual(sinv.taxes[0].account_head, "_Test Account Excise Duty - _TC")
|
||||
self.assertEqual(sinv.total_taxes_and_charges, 5)
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
|
||||
@@ -47,7 +47,7 @@ def get_columns(filters, trans):
|
||||
def validate_filters(filters):
|
||||
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
|
||||
if not filters.get(f.lower().replace(" ", "_")):
|
||||
frappe.throw(_("{0} is mandatory").format(f))
|
||||
frappe.throw(_("{0} is mandatory").format(_(f)))
|
||||
|
||||
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
|
||||
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))
|
||||
@@ -97,10 +97,13 @@ def get_data(filters, conditions):
|
||||
elif filters.get("group_by") == "Supplier":
|
||||
sel_col = "t1.supplier"
|
||||
|
||||
if filters.get("based_on") in ["Item", "Customer", "Supplier"]:
|
||||
if filters.get("based_on") in ["Customer", "Supplier"]:
|
||||
inc = 3
|
||||
elif filters.get("based_on") in ["Item"]:
|
||||
inc = 2
|
||||
else:
|
||||
inc = 1
|
||||
|
||||
data1 = frappe.db.sql(
|
||||
""" select {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
where t2.parent = t1.name and t1.company = {} and {} between {} and {} and
|
||||
@@ -157,7 +160,7 @@ def get_data(filters, conditions):
|
||||
|
||||
# get data for group_by filter
|
||||
row1 = frappe.db.sql(
|
||||
""" select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
""" select t4.default_currency AS currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
where t2.parent = t1.name and t1.company = {} and {} between {} and {}
|
||||
and t1.docstatus = 1 and {} = {} and {} = {} {} {}
|
||||
""".format(
|
||||
@@ -330,11 +333,20 @@ def based_wise_columns_query(based_on, trans):
|
||||
based_on_details["addl_tables"] = ""
|
||||
|
||||
elif based_on == "Customer":
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Customer:Link/Customer:120",
|
||||
"Territory:Link/Territory:120",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.customer_name, t1.territory, "
|
||||
if trans == "Quotation":
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Party:Link/Customer:120",
|
||||
"Party Name:Data:120",
|
||||
"Territory:Link/Territory:120",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.party_name, t1.customer_name, t1.territory,"
|
||||
else:
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Customer:Link/Customer:120",
|
||||
"Customer Name:Data:120",
|
||||
"Territory:Link/Territory:120",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.customer, t1.customer_name, t1.territory,"
|
||||
based_on_details["based_on_group_by"] = "t1.party_name" if trans == "Quotation" else "t1.customer"
|
||||
based_on_details["addl_tables"] = ""
|
||||
|
||||
@@ -347,9 +359,10 @@ def based_wise_columns_query(based_on, trans):
|
||||
elif based_on == "Supplier":
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Supplier:Link/Supplier:120",
|
||||
"Supplier Name:Data:120",
|
||||
"Supplier Group:Link/Supplier Group:140",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.supplier, t3.supplier_group,"
|
||||
based_on_details["based_on_select"] = "t1.supplier, t1.supplier_name, t3.supplier_group,"
|
||||
based_on_details["based_on_group_by"] = "t1.supplier"
|
||||
based_on_details["addl_tables"] = ",`tabSupplier` t3"
|
||||
based_on_details["addl_tables_relational_cond"] = " and t1.supplier = t3.name"
|
||||
@@ -381,8 +394,12 @@ def based_wise_columns_query(based_on, trans):
|
||||
else:
|
||||
frappe.throw(_("Project-wise data is not available for Quotation"))
|
||||
|
||||
based_on_details["based_on_select"] += "t1.currency,"
|
||||
based_on_details["based_on_select"] += "t4.default_currency as currency,"
|
||||
based_on_details["based_on_cols"].append("Currency:Link/Currency:120")
|
||||
based_on_details["addl_tables"] += ", `tabCompany` t4"
|
||||
based_on_details["addl_tables_relational_cond"] = (
|
||||
based_on_details.get("addl_tables_relational_cond", "") + " and t1.company = t4.name"
|
||||
)
|
||||
|
||||
return based_on_details
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "CON-.YYYY.-.#####",
|
||||
"creation": "2018-04-12 06:32:04.582486",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -256,10 +257,11 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-23 13:54:03.346537",
|
||||
"modified": "2025-06-19 17:48:45.049007",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Contract",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -324,9 +326,12 @@
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "party_type, party_name, contract_template",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "party_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
|
||||
@@ -46,19 +46,6 @@ class Contract(Document):
|
||||
status: DF.Literal["Unsigned", "Active", "Inactive"]
|
||||
# end: auto-generated types
|
||||
|
||||
def autoname(self):
|
||||
name = self.party_name
|
||||
|
||||
if self.contract_template:
|
||||
name += f" - {self.contract_template} Agreement"
|
||||
|
||||
# If identical, append contract name with the next number in the iteration
|
||||
if frappe.db.exists("Contract", name):
|
||||
count = len(frappe.get_all("Contract", filters={"name": ["like", f"%{name}%"]}))
|
||||
name = f"{name} - {count}"
|
||||
|
||||
self.name = _(name)
|
||||
|
||||
def validate(self):
|
||||
self.set_missing_values()
|
||||
self.validate_dates()
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -512,11 +512,12 @@
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2025-01-31 13:40:08.094759",
|
||||
"modified": "2025-06-26 11:02:01.158901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -575,6 +576,7 @@
|
||||
"role": "Sales User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "lead_name,lead_owner,status",
|
||||
"sender_field": "email_id",
|
||||
"sender_name_field": "lead_name",
|
||||
|
||||
@@ -323,7 +323,8 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.customer_type = "Individual"
|
||||
target.customer_name = source.lead_name
|
||||
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
if not target.customer_group:
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Lead",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -622,10 +622,11 @@
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified": "2025-06-26 11:16:13.665866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
@@ -657,6 +658,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status,transaction_date,party_name,opportunity_type,territory,company",
|
||||
"sender_field": "contact_email",
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Opportunity",
|
||||
"dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]",
|
||||
"filters_json": "[]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Open Opportunity",
|
||||
"modified": "2020-07-22 16:16:16.420446",
|
||||
"modified": "2025-06-24 11:10:17.468713",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Open Opportunity",
|
||||
"owner": "Administrator",
|
||||
"show_full_number": 0,
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Daily",
|
||||
"type": "Document Type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_columns():
|
||||
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
|
||||
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
|
||||
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State/Province"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Country"),
|
||||
"fieldname": "country",
|
||||
|
||||
@@ -57,7 +57,6 @@ setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo"
|
||||
setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test"
|
||||
|
||||
before_install = [
|
||||
"erpnext.setup.install.check_setup_wizard_not_completed",
|
||||
"erpnext.setup.install.check_frappe_version",
|
||||
]
|
||||
after_install = "erpnext.setup.install.after_install"
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
"fieldname": "quantity",
|
||||
"fieldtype": "Float",
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "quantity",
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
@@ -637,7 +638,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-03 16:24:47.518411",
|
||||
"modified": "2025-06-16 16:13:22.497695",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
@@ -670,6 +671,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item, item_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -987,7 +987,7 @@ class BOM(WebsiteGenerator):
|
||||
self.transfer_material_against = "Work Order"
|
||||
if not self.transfer_material_against and not self.is_new():
|
||||
frappe.throw(
|
||||
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
|
||||
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
|
||||
@@ -51,9 +51,13 @@ frappe.ui.form.on("Job Card", {
|
||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
|
||||
if (to_request || excess_transfer_allowed) {
|
||||
frm.add_custom_button(__("Material Request"), () => {
|
||||
frm.trigger("make_material_request");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
() => {
|
||||
frm.trigger("make_material_request");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
// check if any row has untransferred materials
|
||||
@@ -61,9 +65,13 @@ frappe.ui.form.on("Job Card", {
|
||||
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
||||
|
||||
if (to_transfer || excess_transfer_allowed) {
|
||||
frm.add_custom_button(__("Material Transfer"), () => {
|
||||
frm.trigger("make_stock_entry");
|
||||
}).addClass("btn-primary");
|
||||
frm.add_custom_button(
|
||||
__("Material Transfer"),
|
||||
() => {
|
||||
frm.trigger("make_stock_entry");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1184,6 +1184,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus < 2)
|
||||
@@ -1927,6 +1928,7 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
|
||||
@@ -2368,6 +2368,105 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
def test_disassembly_order_with_qty_behavior(self):
|
||||
# Create raw material and FG item
|
||||
raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name
|
||||
fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name
|
||||
bom = make_bom(item=fg_item, quantity=10, raw_materials=[raw_item], rm_qty=5)
|
||||
|
||||
# Create and submit a Work Order for 10 qty
|
||||
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
|
||||
|
||||
# create material receipt stock entry for raw material
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
make_stock_entry_test_record(
|
||||
item_code=raw_item,
|
||||
purpose="Material Receipt",
|
||||
target=wo.wip_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
make_stock_entry_test_record(
|
||||
item_code=raw_item,
|
||||
purpose="Material Receipt",
|
||||
target=wo.fg_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
# create material transfer for manufacture stock entry
|
||||
se_for_material_tranfer_mfr = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
|
||||
)
|
||||
se_for_material_tranfer_mfr.items[0].s_warehouse = wo.wip_warehouse
|
||||
se_for_material_tranfer_mfr.save()
|
||||
se_for_material_tranfer_mfr.submit()
|
||||
|
||||
se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty))
|
||||
se_for_manufacture.submit()
|
||||
|
||||
# Simulate a disassembly stock entry
|
||||
disassemble_qty = 4
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": fg_item,
|
||||
"qty": disassemble_qty,
|
||||
"s_warehouse": wo.fg_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
for bom_item in bom.items:
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": (bom_item.qty / bom.quantity) * disassemble_qty,
|
||||
"t_warehouse": wo.source_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
wo.reload()
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
|
||||
# Assert FG item is present with correct qty
|
||||
finished_good_entry = next((item for item in stock_entry.items if item.item_code == fg_item), None)
|
||||
self.assertIsNotNone(finished_good_entry, "Finished good item missing from stock entry")
|
||||
self.assertEqual(
|
||||
finished_good_entry.qty,
|
||||
disassemble_qty,
|
||||
f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}",
|
||||
)
|
||||
|
||||
# Assert raw materials
|
||||
for item in stock_entry.items:
|
||||
if item.item_code == fg_item:
|
||||
continue
|
||||
bom_item = next((i for i in bom.items if i.item_code == item.item_code), None)
|
||||
if bom_item:
|
||||
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
|
||||
self.assertAlmostEqual(
|
||||
item.qty,
|
||||
expected_qty,
|
||||
places=3,
|
||||
msg=f"Raw item {item.item_code} qty mismatch: expected {expected_qty}, got {item.qty}",
|
||||
)
|
||||
else:
|
||||
self.fail(f"Unexpected item {item.item_code} found in stock entry")
|
||||
|
||||
wo.reload()
|
||||
# Assert disassembled_qty field updated in Work Order
|
||||
self.assertEqual(
|
||||
wo.disassembled_qty,
|
||||
disassemble_qty,
|
||||
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
|
||||
)
|
||||
|
||||
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
@@ -3118,6 +3217,7 @@ def make_wo_order_test_record(**args):
|
||||
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
||||
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
|
||||
wo_order.batch_size = args.batch_size or 0
|
||||
wo_order.status = args.status or "Draft"
|
||||
|
||||
if args.source_warehouse:
|
||||
wo_order.source_warehouse = args.source_warehouse
|
||||
|
||||
@@ -101,6 +101,17 @@ frappe.ui.form.on("Work Order", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sales_order", function () {
|
||||
if (frm.doc.production_item) {
|
||||
return {
|
||||
query: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
|
||||
filters: {
|
||||
production_item: frm.doc.production_item,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// formatter for work order operation
|
||||
frm.set_indicator_formatter("operation", function (doc) {
|
||||
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
|
||||
@@ -481,7 +492,6 @@ frappe.ui.form.on("Work Order", {
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("sales_order", "");
|
||||
frm.trigger("set_sales_order");
|
||||
erpnext.in_production_item_onchange = true;
|
||||
|
||||
$.each(
|
||||
@@ -543,23 +553,6 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.toggle_reqd("transfer_material_against", frm.doc.operations && frm.doc.operations.length > 0);
|
||||
},
|
||||
|
||||
set_sales_order: function (frm) {
|
||||
if (frm.doc.production_item) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
|
||||
args: { production_item: frm.doc.production_item },
|
||||
callback: function (r) {
|
||||
frm.set_query("sales_order", function () {
|
||||
erpnext.in_production_item_onchange = true;
|
||||
return {
|
||||
filters: [["Sales Order", "name", "in", r.message]],
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
additional_operating_cost: function (frm) {
|
||||
erpnext.work_order.calculate_cost(frm.doc);
|
||||
erpnext.work_order.calculate_total_cost(frm);
|
||||
@@ -810,7 +803,7 @@ erpnext.work_order = {
|
||||
get_max_transferable_qty: (frm, purpose) => {
|
||||
let max = 0;
|
||||
if (purpose === "Disassemble") {
|
||||
return flt(frm.doc.produced_qty);
|
||||
return flt(frm.doc.produced_qty - frm.doc.disassembled_qty);
|
||||
}
|
||||
|
||||
if (frm.doc.skip_transfer) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"qty",
|
||||
"material_transferred_for_manufacturing",
|
||||
"produced_qty",
|
||||
"disassembled_qty",
|
||||
"process_loss_qty",
|
||||
"project",
|
||||
"section_break_ndpq",
|
||||
@@ -586,6 +587,14 @@
|
||||
{
|
||||
"fieldname": "section_break_ndpq",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==1",
|
||||
"fieldname": "disassembled_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Disassembled Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
@@ -593,7 +602,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-11 15:47:13.454422",
|
||||
"modified": "2025-06-21 00:55:45.916224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
@@ -629,4 +638,4 @@
|
||||
"title_field": "production_item",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class WorkOrder(Document):
|
||||
company: DF.Link
|
||||
corrective_operation_cost: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
disassembled_qty: DF.Float
|
||||
expected_delivery_date: DF.Date | None
|
||||
fg_warehouse: DF.Link
|
||||
from_wip_warehouse: DF.Check
|
||||
@@ -389,7 +390,7 @@ class WorkOrder(Document):
|
||||
if qty > completed_qty:
|
||||
frappe.throw(
|
||||
_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(
|
||||
self.meta.get_label(fieldname), qty, completed_qty, self.name
|
||||
_(self.meta.get_label(fieldname)), qty, completed_qty, self.name
|
||||
),
|
||||
StockOverProductionError,
|
||||
)
|
||||
@@ -406,6 +407,18 @@ class WorkOrder(Document):
|
||||
self.set_produced_qty_for_sub_assembly_item()
|
||||
self.update_production_plan_status()
|
||||
|
||||
def update_disassembled_qty(self, qty, is_cancel=False):
|
||||
if is_cancel:
|
||||
self.disassembled_qty = max(0, self.disassembled_qty - qty)
|
||||
else:
|
||||
if self.docstatus == 1:
|
||||
self.disassembled_qty += qty
|
||||
|
||||
if not is_cancel and self.disassembled_qty > self.produced_qty:
|
||||
frappe.throw(_("Cannot disassemble more than produced quantity."))
|
||||
|
||||
self.db_set("disassembled_qty", self.disassembled_qty)
|
||||
|
||||
def get_transferred_or_manufactured_qty(self, purpose):
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
query = frappe.qb.from_(table).where(
|
||||
@@ -1064,7 +1077,7 @@ class WorkOrder(Document):
|
||||
self.transfer_material_against = "Work Order"
|
||||
if not self.transfer_material_against:
|
||||
frappe.throw(
|
||||
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
|
||||
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
@@ -1475,7 +1488,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items()
|
||||
stock_entry.get_items(qty, work_order.production_item)
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
@@ -1515,17 +1528,19 @@ def stop_unstop(work_order, status):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def query_sales_order(production_item: str) -> list[str]:
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> list[str]:
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["name"],
|
||||
filters=[
|
||||
["Sales Order", "docstatus", "=", 1],
|
||||
],
|
||||
or_filters=[
|
||||
["Sales Order Item", "item_code", "=", production_item],
|
||||
["Packed Item", "item_code", "=", production_item],
|
||||
["Sales Order Item", "item_code", "=", filters.get("production_item")],
|
||||
["Packed Item", "item_code", "=", filters.get("production_item")],
|
||||
],
|
||||
pluck="name",
|
||||
as_list=True,
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from frappe.utils import cint
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -20,8 +18,7 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
return [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::240",
|
||||
_("Description") + "::300",
|
||||
@@ -32,55 +29,54 @@ def get_columns():
|
||||
_("Enough Parts to Build") + ":Float:200",
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
qty_to_produce = filters.get("qty_to_produce")
|
||||
if cint(qty_to_produce) <= 0:
|
||||
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||
|
||||
if filters.get("show_exploded_view"):
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
|
||||
|
||||
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
|
||||
warehouse = filters.get("warehouse")
|
||||
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
|
||||
|
||||
BOM = frappe.qb.DocType("BOM")
|
||||
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||
BIN = frappe.qb.DocType("Bin")
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
CONDITIONS = ()
|
||||
|
||||
if warehouse_details:
|
||||
CONDITIONS = ExistsCriterion(
|
||||
frappe.qb.from_(WH)
|
||||
.select(WH.name)
|
||||
.where(
|
||||
(WH.lft >= warehouse_details.lft)
|
||||
& (WH.rgt <= warehouse_details.rgt)
|
||||
& (BIN.warehouse == WH.name)
|
||||
)
|
||||
bin_subquery = (
|
||||
frappe.qb.from_(BIN)
|
||||
.join(WH)
|
||||
.on(BIN.warehouse == WH.name)
|
||||
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
|
||||
.where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt))
|
||||
.groupby(BIN.item_code)
|
||||
)
|
||||
else:
|
||||
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||
bin_subquery = (
|
||||
frappe.qb.from_(BIN)
|
||||
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
|
||||
.where(BIN.warehouse == warehouse)
|
||||
.groupby(BIN.item_code)
|
||||
)
|
||||
|
||||
QUERY = (
|
||||
frappe.qb.from_(BOM)
|
||||
.inner_join(BOM_ITEM)
|
||||
.join(BOM_ITEM)
|
||||
.on(BOM.name == BOM_ITEM.parent)
|
||||
.left_join(BIN)
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.left_join(bin_subquery)
|
||||
.on(BOM_ITEM.item_code == bin_subquery.item_code)
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.item_name,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
Sum(BOM_ITEM.stock_qty),
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
BIN.actual_qty.as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
(Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity,
|
||||
bin_subquery.actual_qty,
|
||||
Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
.groupby(BOM_ITEM.item_code)
|
||||
|
||||
@@ -409,3 +409,6 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
erpnext.patches.v15_0.update_pick_list_fields
|
||||
erpnext.patches.v15_0.update_pegged_currencies
|
||||
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
|
||||
|
||||
12
erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py
Normal file
12
erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
pos_invoice_merge_logs = frappe.db.get_all(
|
||||
"POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"]
|
||||
)
|
||||
|
||||
for log in pos_invoice_merge_logs:
|
||||
if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry):
|
||||
company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company")
|
||||
frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company)
|
||||
7
erpnext/patches/v15_0/update_pegged_currencies.py
Normal file
7
erpnext/patches/v15_0/update_pegged_currencies.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.install import update_pegged_currencies
|
||||
|
||||
|
||||
def execute():
|
||||
update_pegged_currencies()
|
||||
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
|
||||
def execute():
|
||||
update_delivery_note()
|
||||
update_pick_list_items()
|
||||
|
||||
|
||||
def update_delivery_note():
|
||||
DN = frappe.qb.DocType("Delivery Note")
|
||||
DNI = frappe.qb.DocType("Delivery Note Item")
|
||||
|
||||
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
|
||||
IfNull(DN.pick_list, "") != ""
|
||||
).run()
|
||||
|
||||
|
||||
def update_pick_list_items():
|
||||
PL = frappe.qb.DocType("Pick List")
|
||||
PLI = frappe.qb.DocType("Pick List Item")
|
||||
|
||||
pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name")
|
||||
|
||||
if not pick_lists:
|
||||
return
|
||||
|
||||
frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run()
|
||||
@@ -202,6 +202,12 @@ frappe.ui.form.on("Project", {
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
collect_progress: function (frm) {
|
||||
if (frm.doc.collect_progress && !frm.doc.subject) {
|
||||
frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name]));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function open_form(frm, doctype, child_doctype, parentfield) {
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"day_to_send",
|
||||
"weekly_time_to_send",
|
||||
"column_break_45",
|
||||
"subject",
|
||||
"message"
|
||||
],
|
||||
"fields": [
|
||||
@@ -447,6 +448,13 @@
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "collect_progress",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"mandatory_depends_on": "collect_progress"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-puzzle-piece",
|
||||
@@ -454,7 +462,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2024-04-24 10:56:16.001032",
|
||||
"modified": "2025-07-03 10:54:30.444139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
@@ -501,6 +509,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "project_name,customer, status, priority, is_active",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -509,4 +518,4 @@
|
||||
"timeline_field": "customer",
|
||||
"title_field": "project_name",
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class Project(Document):
|
||||
sales_order: DF.Link | None
|
||||
second_email: DF.Time | None
|
||||
status: DF.Literal["Open", "Completed", "Cancelled"]
|
||||
subject: DF.Data | None
|
||||
to_time: DF.Time | None
|
||||
total_billable_amount: DF.Currency
|
||||
total_billed_amount: DF.Currency
|
||||
@@ -606,8 +607,6 @@ def send_project_update_email_to_users(project):
|
||||
}
|
||||
).insert()
|
||||
|
||||
subject = "For project %s, update your status" % (project)
|
||||
|
||||
incoming_email_account = frappe.db.get_value(
|
||||
"Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
|
||||
)
|
||||
@@ -615,7 +614,7 @@ def send_project_update_email_to_users(project):
|
||||
frappe.sendmail(
|
||||
recipients=get_users_email(doc),
|
||||
message=doc.message,
|
||||
subject=_(subject),
|
||||
subject=doc.subject,
|
||||
reference_doctype=project_update.doctype,
|
||||
reference_name=project_update.name,
|
||||
reply_to=incoming_email_account,
|
||||
|
||||
@@ -581,7 +581,8 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
|
||||
transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
|
||||
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
|
||||
doctype: frm.doc.doctype
|
||||
}
|
||||
},
|
||||
price_list: frm.doc.price_list,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
|
||||
@@ -8,7 +8,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let me = this;
|
||||
|
||||
this.set_fields_onload_for_line_item();
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
|
||||
@@ -371,6 +371,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"inspection_type": inspection_type,
|
||||
"reference_type": me.frm.doc.doctype,
|
||||
"reference_name": me.frm.doc.name,
|
||||
"child_row_reference": row.doc.name,
|
||||
"item_code": row.doc.item_code,
|
||||
"description": row.doc.description,
|
||||
"item_serial_no": row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
|
||||
@@ -385,7 +386,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
docstatus: ["<", 2],
|
||||
inspection_type: inspection_type,
|
||||
reference_name: doc.name,
|
||||
item_code: d.item_code
|
||||
item_code: d.item_code,
|
||||
child_row_reference : d.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -914,7 +916,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
var get_party_currency = function() {
|
||||
if (me.is_a_mapped_document()) {
|
||||
if (me.is_a_mapped_document() || me.frm.doc.__onload?.load_after_mapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -987,7 +989,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
var party = me.frm.doc[frappe.model.scrub(party_type)];
|
||||
if(party && me.frm.doc.company) {
|
||||
if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc.get(party_account_field))) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
@@ -2427,12 +2429,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
fields: fields,
|
||||
primary_action: function () {
|
||||
const data = dialog.get_values();
|
||||
const selected_data = data.items.filter(item => item?.__checked == 1 );
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
docname: me.frm.doc.name,
|
||||
items: data.items
|
||||
items: selected_data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user