mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-24 13:15:03 +00:00
Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3eddeb26 | ||
|
|
43dadc763c | ||
|
|
64c185f097 | ||
|
|
1390c86fc4 | ||
|
|
9d2ef4d3e8 | ||
|
|
6840f6cb26 | ||
|
|
61280e6072 | ||
|
|
b7d70ac928 | ||
|
|
826577c88f | ||
|
|
5fa185d480 | ||
|
|
75271ca1a0 | ||
|
|
f03e58f5f6 | ||
|
|
596110dd65 | ||
|
|
76018f5b9c | ||
|
|
1019f6d158 | ||
|
|
fc0db1941a | ||
|
|
f25b38caf5 | ||
|
|
edf1fcb742 | ||
|
|
4b66fcad64 | ||
|
|
334c4d0676 | ||
|
|
ac2ef21896 | ||
|
|
b529dc7dd1 | ||
|
|
4b24f8bc04 | ||
|
|
0c2e9480cb | ||
|
|
901f83edd5 | ||
|
|
80d4dc2016 | ||
|
|
3a279db06b | ||
|
|
3053254db7 | ||
|
|
d135193f6c | ||
|
|
183ac41550 | ||
|
|
322fbe92ee | ||
|
|
367b7eeeba | ||
|
|
dd8fff6d43 | ||
|
|
3e3bdf7491 | ||
|
|
9e99eda3c3 | ||
|
|
05b9432f6d | ||
|
|
422824b9e7 | ||
|
|
2d2140aad0 | ||
|
|
a24fe951ed | ||
|
|
2de69e2b12 | ||
|
|
4bae4194ab | ||
|
|
7b3a78e04e | ||
|
|
219310e817 | ||
|
|
298a5699f1 | ||
|
|
bf34c94483 | ||
|
|
a7d8202b4e | ||
|
|
2ae94b2af2 | ||
|
|
e607f3c78d | ||
|
|
d69361b1c9 | ||
|
|
949d7f4b53 | ||
|
|
01dfea3ffa | ||
|
|
8b9860902c | ||
|
|
3d8eac9b5a | ||
|
|
a8fe0e89a8 | ||
|
|
9b52d89e03 | ||
|
|
e28c1e9c4b | ||
|
|
1e16a987dd | ||
|
|
c045c9a6dd | ||
|
|
8141c6504e | ||
|
|
ffacf4222b | ||
|
|
aea8271f7e | ||
|
|
72bc539ffd | ||
|
|
926fd41a2b | ||
|
|
f2bc064da2 | ||
|
|
555be2be11 | ||
|
|
8da28dcfb2 | ||
|
|
5741f32080 | ||
|
|
22b17de2b4 | ||
|
|
96062dec10 | ||
|
|
29fc975fb8 | ||
|
|
0052ca9173 | ||
|
|
54791e938b | ||
|
|
709be13e82 | ||
|
|
7893a957cb | ||
|
|
a9146efc17 | ||
|
|
aee2cc2e03 | ||
|
|
25fe08eb74 | ||
|
|
307dcea097 | ||
|
|
0dae0a05d4 | ||
|
|
886256c86b | ||
|
|
372a7e905c | ||
|
|
3f820734b6 | ||
|
|
0a41ccda99 | ||
|
|
18500b8e3a | ||
|
|
bebd70d752 | ||
|
|
527781a588 | ||
|
|
4b8b3ee46a | ||
|
|
85aca0ef55 | ||
|
|
55dc157694 | ||
|
|
c45d11cd60 | ||
|
|
cfab956811 | ||
|
|
d1ba12f581 | ||
|
|
0d2ef0df7d | ||
|
|
b5a2e5a375 | ||
|
|
2ffe7d5838 | ||
|
|
62fc42803f | ||
|
|
29ee2d46f0 | ||
|
|
b741b2a285 | ||
|
|
1754adfcd6 | ||
|
|
5f1d6ede31 | ||
|
|
cf2651dd85 | ||
|
|
29158652db | ||
|
|
4968395372 | ||
|
|
8e70aeae4a | ||
|
|
4ac174703c | ||
|
|
804f1d4772 | ||
|
|
609a0b81ae | ||
|
|
cc09d0d218 | ||
|
|
43eec001ee | ||
|
|
f161e59cd7 | ||
|
|
076bf17439 | ||
|
|
cb64c73c9e | ||
|
|
ce8b423ad6 | ||
|
|
e250dcc7c8 | ||
|
|
ad228d80d5 | ||
|
|
6d098b7302 | ||
|
|
ddd1ca7f7c | ||
|
|
4c9ce1b188 | ||
|
|
1de66e56ee | ||
|
|
5e3810b12a | ||
|
|
84a8bb3ce5 | ||
|
|
ef16313e0a | ||
|
|
61852bd3f6 | ||
|
|
cd79d33db2 | ||
|
|
4f7e0d2955 | ||
|
|
e07bdcee79 | ||
|
|
4055ef92b5 | ||
|
|
2751a9a38c | ||
|
|
a2b21c7570 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.29.3"
|
||||
__version__ = "15.31.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"post_change_gl_entries",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
@@ -71,7 +73,9 @@
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length"
|
||||
"receivable_payable_remarks_length",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -462,6 +466,29 @@
|
||||
"fieldname": "enable_immutable_ledger",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Immutable Ledger"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gjcc",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this option to calculate daily depreciation by considering the total number of days in the entire depreciation period, (including leap years) while using daily pro-rata based depreciation",
|
||||
"fieldname": "calculate_depr_using_total_days",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate daily depreciation using total days in depreciation period"
|
||||
},
|
||||
{
|
||||
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
|
||||
"fieldname": "payment_request_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -469,7 +496,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-11 23:19:44.673975",
|
||||
"modified": "2024-07-26 06:48:52.714630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -498,4 +525,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ class AccountsSettings(Document):
|
||||
book_deferred_entries_based_on: DF.Literal["Days", "Months"]
|
||||
book_deferred_entries_via_journal_entry: DF.Check
|
||||
book_tax_discount_loss: DF.Check
|
||||
calculate_depr_using_total_days: DF.Check
|
||||
check_supplier_invoice_uniqueness: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
|
||||
@@ -25,30 +25,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
refresh: function (frm) {
|
||||
erpnext.toggle_naming_series();
|
||||
|
||||
if (frm.doc.repost_required && frm.doc.docstatus === 1) {
|
||||
frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
frm.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted."));
|
||||
frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Ledger"),
|
||||
|
||||
@@ -64,8 +64,7 @@
|
||||
"stock_entry",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from",
|
||||
"repost_required"
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -544,15 +543,6 @@
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -567,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
@@ -618,4 +608,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,7 @@ class JournalEntry(AccountsController):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import (
|
||||
JournalEntryAccount,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -197,13 +195,10 @@ class JournalEntry(AccountsController):
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
self.needs_repost = self.check_if_fields_updated(
|
||||
fields_to_check=[], child_tables={"accounts": []}
|
||||
)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
|
||||
@@ -454,12 +454,9 @@ class TestJournalEntry(unittest.TestCase):
|
||||
# Change cost center for bank account - _Test Cost Center for BS Account
|
||||
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
|
||||
jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
jv.save()
|
||||
|
||||
# Check if repost flag gets set on update after submit
|
||||
self.assertTrue(jv.repost_required)
|
||||
jv.repost_accounting_entries()
|
||||
|
||||
# Check GL entries after reposting
|
||||
jv.load_from_db()
|
||||
self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
|
||||
@@ -481,6 +478,43 @@ class TestJournalEntry(unittest.TestCase):
|
||||
for field in self.fields:
|
||||
self.assertEqual(self.expected_gle[i][field], gl_entries[i][field])
|
||||
|
||||
def test_negative_debit_and_credit_with_same_account_head(self):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
# Create JV with defaut cost center - _Test Cost Center
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Bank - _TC", 100 * -1, save=True)
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"debit": 100 * -1,
|
||||
"credit": 100 * -1,
|
||||
"debit_in_account_currency": 100 * -1,
|
||||
"credit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
jv.flags.ignore_validate = True
|
||||
jv.save()
|
||||
|
||||
self.assertEqual(len(jv.accounts), 3)
|
||||
|
||||
gl_map = jv.build_gl_map()
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit_in_account_currency, 100 * -1)
|
||||
self.assertEqual(row.credit_in_account_currency, 100 * -1)
|
||||
|
||||
gl_map = process_gl_map(gl_map, False)
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
|
||||
// payment Entry
|
||||
if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") {
|
||||
frm.add_custom_button(__("Create Payment Entries"), function () {
|
||||
frm.add_custom_button(__("Create Journal Entries"), function () {
|
||||
frm.trigger("make_payment_records");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,6 +267,7 @@ class PaymentReconciliation(Document):
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
conditions.append(doc.outstanding_amount != 0)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
@@ -1805,6 +1805,46 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
def test_cr_note_payment_limit_filter(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
for _ in range(6):
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 6)
|
||||
self.assertEqual(len(pr.payments), 6)
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
# Limit should not affect in fetching the unallocated cr_note
|
||||
pr.invoice_limit = 5
|
||||
pr.payment_limit = 5
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
@@ -395,7 +396,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-27 09:51:42.277638",
|
||||
"modified": "2024-06-20 13:54:55.245774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -433,4 +434,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +500,8 @@ def make_payment_request(**args):
|
||||
if args.order_type == "Shopping Cart" or args.mute_email:
|
||||
pr.flags.mute_email = True
|
||||
|
||||
pr.insert(ignore_permissions=True)
|
||||
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
|
||||
pr.insert(ignore_permissions=True)
|
||||
if args.submit_doc:
|
||||
pr.submit()
|
||||
|
||||
|
||||
@@ -136,18 +136,28 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def check_if_previous_year_closed(self):
|
||||
last_year_closing = add_days(self.year_start_date, -1)
|
||||
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
if not previous_fiscal_year:
|
||||
return
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
if not frappe.db.exists(
|
||||
"GL Entry",
|
||||
{"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0},
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"company": self.company,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
):
|
||||
return
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
if not frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
|
||||
{
|
||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.selling.page.point_of_sale.point_of_sale import get_items
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
@@ -179,6 +183,94 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
accounting_dimension_department.save()
|
||||
disable_dimension()
|
||||
|
||||
def test_merging_into_sales_invoice_for_batched_item(self):
|
||||
frappe.flags.print_message = False
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
item_doc = make_item(
|
||||
"_Test Item With Batch FOR POS Merge Test",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCH-PM-POS-MERGE-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
)
|
||||
|
||||
item_code = item_doc.name
|
||||
se = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
use_serial_batch_fields=0,
|
||||
)
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
pcv_doc.submit()
|
||||
|
||||
piv_merge = frappe.db.get_value("POS Invoice Merge Log", {"pos_closing_entry": pcv_doc.name}, "name")
|
||||
|
||||
self.assertTrue(piv_merge)
|
||||
piv_merge_doc = frappe.get_doc("POS Invoice Merge Log", piv_merge)
|
||||
self.assertTrue(piv_merge_doc.pos_invoices[0].pos_invoice)
|
||||
self.assertTrue(piv_merge_doc.pos_invoices[1].pos_invoice)
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(pos_inv.consolidated_invoice)
|
||||
pos_inv2.load_from_db()
|
||||
self.assertTrue(pos_inv2.consolidated_invoice)
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 0.0)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
frappe.flags.print_message = True
|
||||
|
||||
pcv_doc.reload()
|
||||
pcv_doc.cancel()
|
||||
|
||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
||||
self.assertEqual(batch_qty, 10)
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
pos_inv.reload()
|
||||
pos_inv2.reload()
|
||||
|
||||
pos_inv.cancel()
|
||||
pos_inv2.cancel()
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 10.0)
|
||||
|
||||
|
||||
def init_user_and_profile(**args):
|
||||
user = "test@example.com"
|
||||
|
||||
@@ -229,7 +229,9 @@ class POSInvoice(SalesInvoice):
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
for table_name in ["items", "packed_items"]:
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.submit_serial_batch_bundle(table_name)
|
||||
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
|
||||
@@ -283,10 +285,11 @@ class POSInvoice(SalesInvoice):
|
||||
{"is_cancelled": 1, "voucher_no": ""},
|
||||
)
|
||||
|
||||
frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel()
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
|
||||
def submit_serial_batch_bundle(self):
|
||||
for item in self.items:
|
||||
def submit_serial_batch_bundle(self, table_name):
|
||||
for item in self.get(table_name):
|
||||
if item.serial_and_batch_bundle:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
|
||||
@@ -355,10 +358,16 @@ class POSInvoice(SalesInvoice):
|
||||
error_msg = []
|
||||
for d in self.get("items"):
|
||||
error_msg = ""
|
||||
if d.get("has_serial_no") and not d.serial_and_batch_bundle:
|
||||
if d.get("has_serial_no") and (
|
||||
(not d.use_serial_batch_fields and not d.serial_and_batch_bundle)
|
||||
or (d.use_serial_batch_fields and not d.serial_no)
|
||||
):
|
||||
error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
|
||||
|
||||
elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
|
||||
elif d.get("has_batch_no") and (
|
||||
(not d.use_serial_batch_fields and not d.serial_and_batch_bundle)
|
||||
or (d.use_serial_batch_fields and not d.batch_no)
|
||||
):
|
||||
error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
|
||||
|
||||
if error_msg:
|
||||
|
||||
@@ -780,8 +780,6 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv1.submit()
|
||||
pos_inv1.reload()
|
||||
|
||||
self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle)
|
||||
|
||||
batches = get_auto_batch_nos(
|
||||
frappe._dict({"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
@@ -957,7 +955,7 @@ def create_pos_invoice(**args):
|
||||
pos_inv.set_missing_values()
|
||||
|
||||
bundle_id = None
|
||||
if args.get("batch_no") or args.get("serial_no"):
|
||||
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
|
||||
type_of_transaction = args.type_of_transaction or "Outward"
|
||||
|
||||
if pos_inv.is_return:
|
||||
@@ -998,6 +996,9 @@ def create_pos_invoice(**args):
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields,
|
||||
"serial_no": args.serial_no if args.use_serial_batch_fields else None,
|
||||
"batch_no": args.batch_no if args.use_serial_batch_fields else None,
|
||||
}
|
||||
# append in pos invoice items without item_code by checking flag without_item_code
|
||||
if args.without_item_code:
|
||||
@@ -1023,6 +1024,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.insert()
|
||||
if not args.do_not_submit:
|
||||
pos_inv.submit()
|
||||
if args.use_serial_batch_fields:
|
||||
pos_inv.reload()
|
||||
else:
|
||||
pos_inv.payment_schedule = []
|
||||
else:
|
||||
|
||||
@@ -634,7 +634,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Batch No",
|
||||
"options": "Batch",
|
||||
"print_hide": 1
|
||||
@@ -655,7 +654,6 @@
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
"oldfieldname": "serial_no",
|
||||
@@ -827,7 +825,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.serial_and_batch_bundle",
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -853,7 +851,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 15:50:17.140269",
|
||||
"modified": "2024-05-07 15:56:53.343317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
@@ -863,4 +861,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ class POSInvoiceMergeLog(Document):
|
||||
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs)
|
||||
self.serial_and_batch_bundle_reference_for_pos_invoice()
|
||||
self.cancel_linked_invoices()
|
||||
|
||||
def process_merging_into_sales_invoice(self, data):
|
||||
@@ -191,6 +192,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for i in items:
|
||||
if (
|
||||
i.item_code == item.item_code
|
||||
and not i.serial_and_batch_bundle
|
||||
and not i.serial_no
|
||||
and not i.batch_no
|
||||
and i.uom == item.uom
|
||||
@@ -312,6 +314,12 @@ class POSInvoiceMergeLog(Document):
|
||||
doc.set_status(update=True)
|
||||
doc.save()
|
||||
|
||||
def serial_and_batch_bundle_reference_for_pos_invoice(self):
|
||||
for d in self.pos_invoices:
|
||||
pos_invoice = frappe.get_doc("POS Invoice", d.pos_invoice)
|
||||
for table_name in ["items", "packed_items"]:
|
||||
pos_invoice.set_serial_and_batch_bundle(table_name)
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
if not si_name:
|
||||
|
||||
@@ -77,31 +77,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus === 1) {
|
||||
this.frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
this.frm
|
||||
.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted."));
|
||||
me.frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0) {
|
||||
if (doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
|
||||
@@ -170,7 +170,6 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"auto_repeat",
|
||||
@@ -364,7 +363,8 @@
|
||||
"description": "Once set, this invoice will be on hold till the set date",
|
||||
"fieldname": "release_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Release Date"
|
||||
"label": "Release Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_17",
|
||||
@@ -1603,15 +1603,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
@@ -1639,7 +1630,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-11 11:28:42.802211",
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -159,7 +159,6 @@ class PurchaseInvoice(BuyingController):
|
||||
rejected_warehouse: DF.Link | None
|
||||
release_date: DF.Date | None
|
||||
remarks: DF.SmallText | None
|
||||
repost_required: DF.Check
|
||||
represents_company: DF.Link | None
|
||||
return_against: DF.Link | None
|
||||
rounded_total: DF.Currency
|
||||
@@ -796,18 +795,17 @@ class PurchaseInvoice(BuyingController):
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
@@ -1704,6 +1702,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("release_date", None)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
self.set("advance_tax", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
@@ -1745,8 +1746,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
@@ -1761,7 +1760,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
||||
self.set("advance_tax", [])
|
||||
for tax in advance_taxes:
|
||||
allocated_amount = 0
|
||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
||||
|
||||
@@ -2005,18 +2005,15 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
@change_settings("Buying Settings", {"supplier_group": None})
|
||||
def test_purchase_invoice_without_supplier_group(self):
|
||||
|
||||
@@ -68,31 +68,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus === 1) {
|
||||
this.frm.set_intro(
|
||||
__(
|
||||
"Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."
|
||||
)
|
||||
);
|
||||
this.frm
|
||||
.add_custom_button(__("Repost Accounting Entries"), () => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "repost_accounting_entries",
|
||||
freeze: true,
|
||||
freeze_message: __("Reposting..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Accounting Entries are reposted"));
|
||||
me.frm.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
.removeClass("btn-default")
|
||||
.addClass("btn-warning");
|
||||
}
|
||||
|
||||
if (this.frm.doc.is_return) {
|
||||
this.frm.return_print_format = "Sales Invoice Return";
|
||||
}
|
||||
@@ -157,7 +132,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
const payment_is_overdue = doc.payment_schedule
|
||||
.map((row) => Date.parse(row.due_date) < Date.now())
|
||||
.reduce((prev, current) => prev || current);
|
||||
.reduce((prev, current) => prev || current, false);
|
||||
|
||||
if (payment_is_overdue) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -502,9 +477,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
} else {
|
||||
var me = this;
|
||||
const for_validate = me.frm.doc.is_return ? true : false;
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "set_missing_values",
|
||||
args: {
|
||||
for_validate: for_validate,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
|
||||
@@ -213,7 +213,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -2125,15 +2124,6 @@
|
||||
"label": "Write Off",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "incoterm",
|
||||
"fieldtype": "Link",
|
||||
@@ -2188,7 +2178,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-06-07 16:49:32.458402",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -161,7 +161,6 @@ class SalesInvoice(SellingController):
|
||||
project: DF.Link | None
|
||||
redeem_loyalty_points: DF.Check
|
||||
remarks: DF.SmallText | None
|
||||
repost_required: DF.Check
|
||||
represents_company: DF.Link | None
|
||||
return_against: DF.Link | None
|
||||
rounded_total: DF.Currency
|
||||
@@ -556,7 +555,6 @@ class SalesInvoice(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("repost_required", 0)
|
||||
|
||||
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
@@ -706,24 +704,23 @@ class SalesInvoice(SellingController):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
@@ -1212,6 +1209,8 @@ class SalesInvoice(SellingController):
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
gl_entries = merge_similar_entries(gl_entries)
|
||||
|
||||
@@ -2225,6 +2224,11 @@ def make_inter_company_purchase_invoice(source_name, target_doc=None):
|
||||
return make_inter_company_transaction("Sales Invoice", source_name, target_doc)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def make_regional_gl_entries(gl_entries, doc):
|
||||
return gl_entries
|
||||
|
||||
|
||||
def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
|
||||
@@ -2940,13 +2940,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.items[0].income_account = "Service - _TC"
|
||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
si.load_from_db()
|
||||
self.assertTrue(si.repost_required)
|
||||
|
||||
si.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
@@ -2956,9 +2952,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
si.load_from_db()
|
||||
self.assertFalse(si.repost_required)
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
|
||||
@@ -268,6 +268,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -275,7 +280,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
to_date=tax_details.to_date,
|
||||
party_type=party_type,
|
||||
)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
|
||||
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
|
||||
tax_deducted_on_advances = 0
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
@@ -387,6 +393,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
payment_entry_filters = {
|
||||
"party_type": party_type,
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
"apply_tax_withholding_amount": 1,
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
|
||||
|
||||
|
||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
|
||||
"""
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
|
||||
@@ -310,6 +310,18 @@ def check_if_in_list(gle, gl_map):
|
||||
def toggle_debit_credit_if_negative(gl_map):
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
|
||||
entry.credit *= -1
|
||||
entry.debit *= -1
|
||||
|
||||
if (
|
||||
flt(entry.debit_in_account_currency) < 0
|
||||
and flt(entry.credit_in_account_currency) < 0
|
||||
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
|
||||
):
|
||||
entry.credit_in_account_currency *= -1
|
||||
entry.debit_in_account_currency *= -1
|
||||
|
||||
if flt(entry.debit) < 0:
|
||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||
entry.debit = 0.0
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -139,6 +139,7 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -253,7 +254,7 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if ple.cost_center:
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
@@ -288,13 +289,13 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = True
|
||||
|
||||
@@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def create_payment_entry(self, docname):
|
||||
def create_payment_entry(self, docname, do_not_submit=False):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
if not do_not_submit:
|
||||
pe.submit()
|
||||
return pe
|
||||
|
||||
def create_credit_note(self, docname, do_not_submit=False):
|
||||
credit_note = create_sales_invoice(
|
||||
@@ -955,3 +957,69 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def test_accounts_receivable_output_for_minor_outstanding(self):
|
||||
"""
|
||||
AR/AP should report miniscule outstanding of 0.01. Or else there will be slight difference with General Ledger/Trial Balance
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=self.cash, party_amount=99.99)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [100, 100, 99.99, 0.01]
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
def test_cost_center_on_report_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.cost_center = self.cost_center
|
||||
si.save().submit()
|
||||
|
||||
new_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "East Wing",
|
||||
"parent_cost_center": self.company + " - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_cc.save()
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
pe = self.create_payment_entry(si.name, do_not_submit=True)
|
||||
pe.cost_center = new_cc.name
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [si.name, si.cost_center, 60]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
@@ -123,19 +123,15 @@ def get_provisional_profit_loss(
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
total_assets = flt(asset[0].get(key))
|
||||
effective_liability = 0.00
|
||||
|
||||
if liability or equity:
|
||||
effective_liability = 0.0
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
else:
|
||||
provisional_profit_loss[key] = total_assets
|
||||
|
||||
total_row[key] = provisional_profit_loss[key]
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
total_row[key] = provisional_profit_loss[key] + effective_liability
|
||||
|
||||
if provisional_profit_loss[key]:
|
||||
has_value = True
|
||||
|
||||
@@ -713,7 +713,8 @@ class GrossProfitGenerator:
|
||||
|
||||
def get_average_buying_rate(self, row, item_code):
|
||||
args = row
|
||||
if item_code not in self.average_buying_rate:
|
||||
key = (item_code, row.warehouse)
|
||||
if key not in self.average_buying_rate:
|
||||
args.update(
|
||||
{
|
||||
"voucher_type": row.parenttype,
|
||||
@@ -727,9 +728,9 @@ class GrossProfitGenerator:
|
||||
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
|
||||
|
||||
average_buying_rate = get_incoming_rate(args)
|
||||
self.average_buying_rate[item_code] = flt(average_buying_rate)
|
||||
self.average_buying_rate[key] = flt(average_buying_rate)
|
||||
|
||||
return self.average_buying_rate[item_code]
|
||||
return self.average_buying_rate[key]
|
||||
|
||||
def get_last_purchase_rate(self, item_code, row):
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
@@ -558,3 +558,50 @@ class TestGrossProfit(FrappeTestCase):
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
||||
def test_valuation_rate_without_previous_sle(self):
|
||||
"""
|
||||
Test Valuation rate calculation when stock ledger is empty and invoices are against different warehouses
|
||||
"""
|
||||
stock_settings = frappe.get_doc("Stock Settings")
|
||||
stock_settings.valuation_method = "FIFO"
|
||||
stock_settings.save()
|
||||
|
||||
item = create_item(
|
||||
item_code="_Test Wirebound Notebook",
|
||||
is_stock_item=1,
|
||||
)
|
||||
item.allow_negative_stock = True
|
||||
item.save()
|
||||
self.item = item.item_code
|
||||
|
||||
item.reload()
|
||||
item.valuation_rate = 1900
|
||||
item.save()
|
||||
sinv1 = self.create_sales_invoice(qty=1, rate=2000, posting_date=nowdate(), do_not_submit=True)
|
||||
sinv1.update_stock = 1
|
||||
sinv1.set_warehouse = self.warehouse
|
||||
sinv1.items[0].warehouse = self.warehouse
|
||||
sinv1.save().submit()
|
||||
|
||||
item.reload()
|
||||
item.valuation_rate = 1800
|
||||
item.save()
|
||||
sinv2 = self.create_sales_invoice(qty=1, rate=2000, posting_date=nowdate(), do_not_submit=True)
|
||||
sinv2.update_stock = 1
|
||||
sinv2.set_warehouse = self.finished_warehouse
|
||||
sinv2.items[0].warehouse = self.finished_warehouse
|
||||
sinv2.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
|
||||
item_from_sinv1 = [x for x in data if x.parent_invoice == sinv1.name]
|
||||
self.assertEqual(len(item_from_sinv1), 1)
|
||||
self.assertEqual(1900, item_from_sinv1[0].valuation_rate)
|
||||
|
||||
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
|
||||
self.assertEqual(len(item_from_sinv2), 1)
|
||||
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
|
||||
|
||||
@@ -315,8 +315,9 @@ def apply_conditions(query, pi, pii, filters):
|
||||
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
@@ -353,6 +354,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.where(pii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if filters.get("supplier"):
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWisePurchaseRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
do_not_save=1,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
qty=1,
|
||||
)
|
||||
|
||||
pi = pi.save()
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
|
||||
def test_basic_report_output(self):
|
||||
pi = self.create_purchase_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": pi.items[0].item_code,
|
||||
"invoice": pi.name,
|
||||
"posting_date": getdate(),
|
||||
"supplier": pi.supplier,
|
||||
"credit_to": pi.credit_to,
|
||||
"company": self.company,
|
||||
"expense_account": pi.items[0].expense_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": pi.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -410,8 +410,9 @@ def apply_group_by_conditions(query, si, ii, filters):
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
@@ -459,6 +460,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.qty,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def test_basic_report_output(self):
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": si.items[0].item_code,
|
||||
"invoice": si.name,
|
||||
"posting_date": getdate(),
|
||||
"customer": si.customer,
|
||||
"debit_to": si.debit_to,
|
||||
"company": self.company,
|
||||
"income_account": si.items[0].income_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": si.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total_other_charges": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -1598,6 +1598,18 @@ def auto_create_exchange_rate_revaluation_weekly() -> None:
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def auto_create_exchange_rate_revaluation_monthly() -> None:
|
||||
"""
|
||||
Executed by background job
|
||||
"""
|
||||
companies = frappe.db.get_all(
|
||||
"Company",
|
||||
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Montly"},
|
||||
fields=["name", "submit_err_jv"],
|
||||
)
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
ple_map = []
|
||||
if gl_entries:
|
||||
|
||||
@@ -187,7 +187,7 @@ frappe.ui.form.on("Asset", {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
|
||||
if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) {
|
||||
if (frm.doc.is_composite_asset) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
|
||||
@@ -511,6 +511,8 @@ frappe.ui.form.on("Asset", {
|
||||
frappe.call({
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
asset_name: frm.doc.asset_name,
|
||||
item_code: frm.doc.item_code,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
|
||||
callback: function (r) {
|
||||
@@ -773,11 +775,8 @@ frappe.ui.form.on("Asset Finance Book", {
|
||||
|
||||
depreciation_start_date: function (frm, cdt, cdn) {
|
||||
const book = locals[cdt][cdn];
|
||||
if (
|
||||
frm.doc.available_for_use_date &&
|
||||
book.depreciation_start_date == frm.doc.available_for_use_date
|
||||
) {
|
||||
frappe.msgprint(__("Depreciation Posting Date should not be equal to Available for Use Date."));
|
||||
if (frm.doc.available_for_use_date && book.depreciation_start_date < frm.doc.available_for_use_date) {
|
||||
frappe.msgprint(__("Depreciation Posting Date cannot be before Available-for-use Date"));
|
||||
book.depreciation_start_date = "";
|
||||
frm.refresh_field("finance_books");
|
||||
}
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
"purchase_amount",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from",
|
||||
"capitalized_in"
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -222,7 +221,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)",
|
||||
"depends_on": "eval:!doc.is_composite_asset",
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
@@ -508,14 +507,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Composite Asset"
|
||||
},
|
||||
{
|
||||
"fieldname": "capitalized_in",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Capitalized In",
|
||||
"options": "Asset Capitalization",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus > 0",
|
||||
"fieldname": "total_asset_cost",
|
||||
@@ -589,7 +580,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-21 13:46:21.066483",
|
||||
"modified": "2024-07-07 22:27:14.733839",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -60,7 +60,6 @@ class Asset(AccountsController):
|
||||
available_for_use_date: DF.Date | None
|
||||
booked_fixed_asset: DF.Check
|
||||
calculate_depreciation: DF.Check
|
||||
capitalized_in: DF.Link | None
|
||||
company: DF.Link
|
||||
comprehensive_insurance: DF.Data | None
|
||||
cost_center: DF.Link | None
|
||||
@@ -162,7 +161,7 @@ class Asset(AccountsController):
|
||||
def on_cancel(self):
|
||||
self.validate_cancellation()
|
||||
self.cancel_movement_entries()
|
||||
self.cancel_capitalization()
|
||||
self.reload()
|
||||
self.delete_depreciation_entries()
|
||||
cancel_asset_depr_schedules(self)
|
||||
self.set_status()
|
||||
@@ -268,10 +267,10 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Available for use date is required"))
|
||||
|
||||
for d in self.finance_books:
|
||||
if d.depreciation_start_date == self.available_for_use_date:
|
||||
if getdate(d.depreciation_start_date) < getdate(self.available_for_use_date):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Depreciation Posting Date should not be equal to Available for Use Date."
|
||||
"Depreciation Row {0}: Depreciation Posting Date cannot be before Available-for-use Date"
|
||||
).format(d.idx),
|
||||
title=_("Incorrect Date"),
|
||||
)
|
||||
@@ -524,16 +523,6 @@ class Asset(AccountsController):
|
||||
movement = frappe.get_doc("Asset Movement", movement.get("name"))
|
||||
movement.cancel()
|
||||
|
||||
def cancel_capitalization(self):
|
||||
asset_capitalization = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"},
|
||||
)
|
||||
|
||||
if asset_capitalization:
|
||||
asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
|
||||
asset_capitalization.cancel()
|
||||
|
||||
def delete_depreciation_entries(self):
|
||||
if self.calculate_depreciation:
|
||||
for row in self.get("finance_books"):
|
||||
@@ -872,10 +861,15 @@ def create_asset_repair(asset, asset_name):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_capitalization(asset):
|
||||
def create_asset_capitalization(asset, asset_name, item_code):
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"}
|
||||
{
|
||||
"target_asset": asset,
|
||||
"capitalization_method": "Choose a WIP composite asset",
|
||||
"target_asset_name": asset_name,
|
||||
"target_item_code": item_code,
|
||||
}
|
||||
)
|
||||
return asset_capitalization
|
||||
|
||||
|
||||
@@ -740,7 +740,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
available_for_use_date="2030-06-06",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
opening_accumulated_depreciation=47095.89,
|
||||
opening_accumulated_depreciation=47178.08,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2032-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
@@ -748,7 +748,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
|
||||
expected_schedules = [["2032-12-31", 30000.0, 77178.08], ["2033-06-06", 12821.92, 90000.0]]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
|
||||
@@ -138,22 +138,10 @@ class AssetCapitalization(StockController):
|
||||
"Asset",
|
||||
"Asset Movement",
|
||||
)
|
||||
self.cancel_target_asset()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
super().on_trash()
|
||||
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.db_set("capitalized_in", None)
|
||||
if asset_doc.docstatus == 1:
|
||||
asset_doc.cancel()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
|
||||
@@ -329,8 +317,12 @@ class AssetCapitalization(StockController):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not self.get("stock_items") and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization"))
|
||||
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||
)
|
||||
)
|
||||
|
||||
def validate_item(self, item):
|
||||
from erpnext.stock.doctype.item.item import validate_end_of_life
|
||||
@@ -617,7 +609,6 @@ class AssetCapitalization(StockController):
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||
asset_doc.insert()
|
||||
@@ -653,7 +644,6 @@ class AssetCapitalization(StockController):
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
|
||||
@@ -386,6 +386,56 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_capitalize_only_service_item(self):
|
||||
company = "_Test Company"
|
||||
# Variables
|
||||
|
||||
service_rate = 500
|
||||
service_qty = 2
|
||||
service_amount = 1000
|
||||
|
||||
total_amount = 1000
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Choose a WIP composite asset",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
service_qty=service_qty,
|
||||
service_rate=service_rate,
|
||||
service_expense_account="Expenses Included In Asset Valuation - _TC",
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset_capitalization.service_items[0].amount, service_amount)
|
||||
self.assertEqual(asset_capitalization.service_items_total, service_amount)
|
||||
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 1000.0,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||
}
|
||||
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_gle, expected_gle)
|
||||
|
||||
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||
asset_capitalization.cancel()
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
|
||||
def create_asset_capitalization_data():
|
||||
create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0)
|
||||
|
||||
@@ -552,9 +552,18 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
|
||||
# if not existing asset, from_date = available_for_use_date
|
||||
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
|
||||
# from_date = 01/01/2022
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
prev_depreciation_start_date = add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
|
||||
)
|
||||
from_date = asset_doc.available_for_use_date
|
||||
days = date_diff(prev_depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(prev_depreciation_start_date, row.frequency_of_depreciation)
|
||||
else:
|
||||
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
|
||||
days = date_diff(row.depreciation_start_date, from_date) + 1
|
||||
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
|
||||
if days <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -682,39 +691,75 @@ def get_straight_line_or_manual_depr_amount(
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
if row.daily_prorata_based:
|
||||
amount = (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
)
|
||||
amount = flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
return get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
)
|
||||
else:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
|
||||
depreciation_amount = (
|
||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations)
|
||||
return depreciation_amount
|
||||
|
||||
|
||||
def get_daily_prorata_based_straight_line_depr(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations, amount
|
||||
):
|
||||
total_years = flt(number_of_pending_depreciations * row.frequency_of_depreciation) / 12
|
||||
every_year_depr = amount / total_years
|
||||
daily_depr_amount = get_daily_depr_amount(asset, row, schedule_idx, amount)
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
daily_depr_amount = every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
|
||||
from_date, total_depreciable_days = _get_total_days(
|
||||
row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
|
||||
)
|
||||
return daily_depr_amount * total_depreciable_days
|
||||
|
||||
|
||||
def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")):
|
||||
total_days = (
|
||||
date_diff(
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
flt(
|
||||
row.total_number_of_depreciations
|
||||
- asset.opening_number_of_booked_depreciations
|
||||
- 1
|
||||
)
|
||||
* row.frequency_of_depreciation,
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(row.frequency_of_depreciation * (asset.opening_number_of_booked_depreciations + 1))
|
||||
* -1,
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
|
||||
return amount / total_days
|
||||
else:
|
||||
total_years = (
|
||||
flt(
|
||||
(row.total_number_of_depreciations - row.total_number_of_booked_depreciations)
|
||||
* row.frequency_of_depreciation
|
||||
)
|
||||
/ 12
|
||||
)
|
||||
|
||||
every_year_depr = amount / total_years
|
||||
|
||||
year_start_date = add_years(
|
||||
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
|
||||
)
|
||||
year_end_date = add_days(add_years(year_start_date, 1), -1)
|
||||
|
||||
return every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
|
||||
|
||||
|
||||
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
||||
return (
|
||||
@@ -867,7 +912,7 @@ def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
|
||||
|
||||
|
||||
def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
|
||||
""" "
|
||||
"""
|
||||
Returns monthly depreciation amount when year changes
|
||||
1. Calculate per day depr based on new year
|
||||
2. Calculate monthly amount based on new per day amount
|
||||
|
||||
@@ -75,6 +75,178 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_for_slm_for_existing_asset_daily_pro_rata_enabled(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1)
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-10-10",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=9,
|
||||
opening_accumulated_depreciation=265,
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
daily_prorata_based=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2024-07-31", 31.0, 296.0],
|
||||
["2024-08-31", 31.0, 327.0],
|
||||
["2024-09-30", 30.0, 357.0],
|
||||
["2024-10-31", 31.0, 388.0],
|
||||
["2024-11-30", 30.0, 418.0],
|
||||
["2024-12-31", 31.0, 449.0],
|
||||
["2025-01-31", 31.0, 480.0],
|
||||
["2025-02-28", 28.0, 508.0],
|
||||
["2025-03-31", 31.0, 539.0],
|
||||
["2025-04-30", 30.0, 569.0],
|
||||
["2025-05-31", 31.0, 600.0],
|
||||
["2025-06-30", 30.0, 630.0],
|
||||
["2025-07-31", 31.0, 661.0],
|
||||
["2025-08-31", 31.0, 692.0],
|
||||
["2025-09-30", 30.0, 722.0],
|
||||
["2025-10-10", 9.0, 731.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0)
|
||||
|
||||
def test_schedule_for_slm_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-10-10",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=9,
|
||||
opening_accumulated_depreciation=265.30,
|
||||
depreciation_start_date="2024-07-31",
|
||||
total_number_of_depreciations=24,
|
||||
frequency_of_depreciation=1,
|
||||
gross_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2024-07-31", 30.46, 295.76],
|
||||
["2024-08-31", 30.46, 326.22],
|
||||
["2024-09-30", 30.46, 356.68],
|
||||
["2024-10-31", 30.46, 387.14],
|
||||
["2024-11-30", 30.46, 417.6],
|
||||
["2024-12-31", 30.46, 448.06],
|
||||
["2025-01-31", 30.46, 478.52],
|
||||
["2025-02-28", 30.46, 508.98],
|
||||
["2025-03-31", 30.46, 539.44],
|
||||
["2025-04-30", 30.46, 569.9],
|
||||
["2025-05-31", 30.46, 600.36],
|
||||
["2025-06-30", 30.46, 630.82],
|
||||
["2025-07-31", 30.46, 661.28],
|
||||
["2025-08-31", 30.46, 691.74],
|
||||
["2025-09-30", 30.46, 722.2],
|
||||
["2025-10-10", 8.8, 731.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_schedule_sl_method_for_existing_asset_with_frequency_of_3_months(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-11-01",
|
||||
is_existing_asset=1,
|
||||
opening_number_of_booked_depreciations=4,
|
||||
opening_accumulated_depreciation=223.15,
|
||||
depreciation_start_date="2024-12-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=3,
|
||||
gross_purchase_amount=731,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2024-12-31", 60.92, 284.07],
|
||||
["2025-03-31", 60.92, 344.99],
|
||||
["2025-06-30", 60.92, 405.91],
|
||||
["2025-09-30", 60.92, 466.83],
|
||||
["2025-12-31", 60.92, 527.75],
|
||||
["2026-03-31", 60.92, 588.67],
|
||||
["2026-06-30", 60.92, 649.59],
|
||||
["2026-09-30", 60.92, 710.51],
|
||||
["2026-11-01", 20.49, 731.0],
|
||||
]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# Enable Checkbox to Calculate depreciation using total days in depreciation period
|
||||
def test_daily_prorata_based_depr_after_enabling_configuration(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
daily_prorata_based=1,
|
||||
gross_purchase_amount=1096,
|
||||
available_for_use_date="2020-01-15",
|
||||
depreciation_start_date="2020-01-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=36,
|
||||
)
|
||||
|
||||
expected_schedule = [
|
||||
["2020-01-31", 17.0, 17.0],
|
||||
["2020-02-29", 29.0, 46.0],
|
||||
["2020-03-31", 31.0, 77.0],
|
||||
["2020-04-30", 30.0, 107.0],
|
||||
["2020-05-31", 31.0, 138.0],
|
||||
["2020-06-30", 30.0, 168.0],
|
||||
["2020-07-31", 31.0, 199.0],
|
||||
["2020-08-31", 31.0, 230.0],
|
||||
["2020-09-30", 30.0, 260.0],
|
||||
["2020-10-31", 31.0, 291.0],
|
||||
["2020-11-30", 30.0, 321.0],
|
||||
["2020-12-31", 31.0, 352.0],
|
||||
["2021-01-31", 31.0, 383.0],
|
||||
["2021-02-28", 28.0, 411.0],
|
||||
["2021-03-31", 31.0, 442.0],
|
||||
["2021-04-30", 30.0, 472.0],
|
||||
["2021-05-31", 31.0, 503.0],
|
||||
["2021-06-30", 30.0, 533.0],
|
||||
["2021-07-31", 31.0, 564.0],
|
||||
["2021-08-31", 31.0, 595.0],
|
||||
["2021-09-30", 30.0, 625.0],
|
||||
["2021-10-31", 31.0, 656.0],
|
||||
["2021-11-30", 30.0, 686.0],
|
||||
["2021-12-31", 31.0, 717.0],
|
||||
["2022-01-31", 31.0, 748.0],
|
||||
["2022-02-28", 28.0, 776.0],
|
||||
["2022-03-31", 31.0, 807.0],
|
||||
["2022-04-30", 30.0, 837.0],
|
||||
["2022-05-31", 31.0, 868.0],
|
||||
["2022-06-30", 30.0, 898.0],
|
||||
["2022-07-31", 31.0, 929.0],
|
||||
["2022-08-31", 31.0, 960.0],
|
||||
["2022-09-30", 30.0, 990.0],
|
||||
["2022-10-31", 31.0, 1021.0],
|
||||
["2022-11-30", 30.0, 1051.0],
|
||||
["2022-12-31", 31.0, 1082.0],
|
||||
["2023-01-15", 14.0, 1096.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
for d in get_depr_schedule(asset.name, "Draft")
|
||||
]
|
||||
self.assertEqual(schedules, expected_schedule)
|
||||
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0)
|
||||
|
||||
# Test for Written Down Value Method
|
||||
# Frequency of deprciation = 3
|
||||
def test_for_daily_prorata_based_depreciation_wdv_method_frequency_3_months(self):
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
frappe.ui.form.on("Asset Maintenance", {
|
||||
setup: (frm) => {
|
||||
frm.set_query("asset_name", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("assign_to", "asset_maintenance_tasks", function (doc) {
|
||||
return {
|
||||
query: "erpnext.assets.doctype.asset_maintenance.asset_maintenance.get_team_members",
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
"fieldname": "supplier_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Supplier Type",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -65,7 +65,7 @@ class Supplier(TransactionBase):
|
||||
supplier_name: DF.Data
|
||||
supplier_primary_address: DF.Link | None
|
||||
supplier_primary_contact: DF.Link | None
|
||||
supplier_type: DF.Literal["Company", "Individual", "Proprietorship", "Partnership"]
|
||||
supplier_type: DF.Literal["Company", "Individual", "Partnership"]
|
||||
tax_category: DF.Link | None
|
||||
tax_id: DF.Data | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
|
||||
@@ -39,16 +39,14 @@ def validate_filters(filters):
|
||||
def get_data(filters):
|
||||
po = frappe.qb.DocType("Purchase Order")
|
||||
po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pi_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
.from_(po_item)
|
||||
.inner_join(po_item)
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on(pi_item.po_detail == po_item.name)
|
||||
.left_join(pi)
|
||||
.on(pi.name == pi_item.parent)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
@@ -72,7 +70,6 @@ def get_data(filters):
|
||||
po_item.name,
|
||||
)
|
||||
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
|
||||
.where(pi.docstatus == 1)
|
||||
.groupby(po_item.name)
|
||||
.orderby(po.transaction_date)
|
||||
)
|
||||
|
||||
@@ -2489,16 +2489,12 @@ class AccountsController(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -541,7 +541,9 @@ class BuyingController(SubcontractingController):
|
||||
"actual_qty": flt(pr_qty),
|
||||
"serial_and_batch_bundle": (
|
||||
d.serial_and_batch_bundle
|
||||
if not self.is_internal_transfer() or self.is_return
|
||||
if not self.is_internal_transfer()
|
||||
or self.is_return
|
||||
or (self.is_internal_transfer() and self.docstatus == 2)
|
||||
else self.get_package_for_target_warehouse(
|
||||
d, type_of_transaction=type_of_transaction
|
||||
)
|
||||
@@ -580,6 +582,14 @@ class BuyingController(SubcontractingController):
|
||||
(not cint(self.is_return) and self.docstatus == 2)
|
||||
or (cint(self.is_return) and self.docstatus == 1)
|
||||
):
|
||||
serial_and_batch_bundle = None
|
||||
if self.is_internal_transfer() and self.docstatus == 2:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": d.name, "warehouse": d.warehouse},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
@@ -589,7 +599,7 @@ class BuyingController(SubcontractingController):
|
||||
"serial_and_batch_bundle": (
|
||||
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
|
||||
if self.is_internal_transfer() and self.is_return
|
||||
else None
|
||||
else serial_and_batch_bundle
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -41,7 +41,8 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
if not args:
|
||||
attribute_args = {k: v for k, v in args.items() if k != "use_template_image"}
|
||||
if not attribute_args:
|
||||
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
||||
|
||||
return find_variant(template, args, variant)
|
||||
@@ -197,7 +198,8 @@ def find_variant(template, args, variant_item_code=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_variant(item, args):
|
||||
def create_variant(item, args, use_template_image=False):
|
||||
use_template_image = frappe.parse_json(use_template_image)
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -211,13 +213,18 @@ def create_variant(item, args):
|
||||
|
||||
variant.set("attributes", variant_attributes)
|
||||
copy_attributes_to_variant(template, variant)
|
||||
|
||||
if use_template_image and template.image:
|
||||
variant.image = template.image
|
||||
|
||||
make_variant_item_code(template.item_code, template.item_name, variant)
|
||||
|
||||
return variant
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def enqueue_multiple_variant_creation(item, args):
|
||||
def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||
use_template_image = frappe.parse_json(use_template_image)
|
||||
# There can be innumerable attribute combinations, enqueue
|
||||
if isinstance(args, str):
|
||||
variants = json.loads(args)
|
||||
@@ -228,27 +235,31 @@ def enqueue_multiple_variant_creation(item, args):
|
||||
frappe.throw(_("Please do not create more than 500 items at a time"))
|
||||
return
|
||||
if total_variants < 10:
|
||||
return create_multiple_variants(item, args)
|
||||
return create_multiple_variants(item, args, use_template_image)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
"erpnext.controllers.item_variant.create_multiple_variants",
|
||||
item=item,
|
||||
args=args,
|
||||
use_template_image=use_template_image,
|
||||
now=frappe.flags.in_test,
|
||||
)
|
||||
return "queued"
|
||||
|
||||
|
||||
def create_multiple_variants(item, args):
|
||||
def create_multiple_variants(item, args, use_template_image=False):
|
||||
count = 0
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
template_item = frappe.get_doc("Item", item)
|
||||
args_set = generate_keyed_value_combinations(args)
|
||||
|
||||
for attribute_values in args_set:
|
||||
if not get_variant(item, args=attribute_values):
|
||||
variant = create_variant(item, attribute_values)
|
||||
if use_template_image and template_item.image:
|
||||
variant.image = template_item.image
|
||||
variant.save()
|
||||
count += 1
|
||||
|
||||
|
||||
@@ -640,6 +640,12 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
def update_terms(source_doc, target_doc, source_parent):
|
||||
target_doc.payment_amount = -source_doc.payment_amount
|
||||
|
||||
def item_condition(doc):
|
||||
if return_against_rejected_qty:
|
||||
return doc.rejected_qty
|
||||
|
||||
return doc.qty
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
doctype,
|
||||
source_name,
|
||||
@@ -654,6 +660,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
"doctype": doctype + " Item",
|
||||
"field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"},
|
||||
"postprocess": update_item,
|
||||
"condition": item_condition,
|
||||
},
|
||||
"Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms},
|
||||
},
|
||||
|
||||
@@ -538,7 +538,9 @@ class SellingController(StockController):
|
||||
self.make_sl_entries(sl_entries)
|
||||
|
||||
def get_sle_for_source_warehouse(self, item_row):
|
||||
serial_and_batch_bundle = item_row.serial_and_batch_bundle
|
||||
serial_and_batch_bundle = (
|
||||
item_row.serial_and_batch_bundle if not self.is_internal_transfer() else None
|
||||
)
|
||||
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
|
||||
if self.docstatus == 1:
|
||||
serial_and_batch_bundle = self.make_package_for_transfer(
|
||||
|
||||
@@ -908,6 +908,7 @@ class SubcontractingController(StockController):
|
||||
item,
|
||||
{
|
||||
"item_code": item.rm_item_code,
|
||||
"incoming_rate": item.rate if self.is_return else 0,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")),
|
||||
"dependant_sle_voucher_detail_no": item.reference_name,
|
||||
|
||||
@@ -456,6 +456,7 @@ scheduler_events = {
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -365,6 +365,12 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
||||
|
||||
|
||||
def get_parent_row_no(doc, name):
|
||||
for row in doc.items:
|
||||
if row.name == name:
|
||||
return row.idx
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
@@ -375,6 +381,11 @@ def add_item(**kwargs):
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
item_info = get_item_details(kwargs.item_code)
|
||||
|
||||
parent_row_no = ""
|
||||
if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"uom": item_info.stock_uom,
|
||||
@@ -383,6 +394,9 @@ def add_item(**kwargs):
|
||||
}
|
||||
)
|
||||
|
||||
if parent_row_no:
|
||||
kwargs.update({"parent_row_no": parent_row_no})
|
||||
|
||||
doc.append("items", kwargs)
|
||||
doc.save()
|
||||
|
||||
|
||||
@@ -254,15 +254,16 @@ erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
erpnext.patches.v15_0.delete_taxjar_doctypes
|
||||
erpnext.patches.v15_0.delete_ecommerce_doctypes
|
||||
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
||||
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
|
||||
erpnext.patches.v15_0.saudi_depreciation_warning
|
||||
erpnext.patches.v15_0.delete_saudi_doctypes
|
||||
erpnext.patches.v14_0.show_loan_management_deprecation_warning
|
||||
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
|
||||
erpnext.patches.v14_0.update_proprietorship_to_individual
|
||||
|
||||
[post_model_sync]
|
||||
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024
|
||||
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
||||
@@ -362,7 +363,6 @@ erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype in ["Customer", "Supplier"]:
|
||||
field = doctype.lower() + "_type"
|
||||
frappe.db.set_value(doctype, {field: "Proprietorship"}, field, "Individual")
|
||||
@@ -1,11 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
cancelled_asset_capitalizations = frappe.get_all(
|
||||
"Asset Capitalization",
|
||||
filters={"docstatus": 2},
|
||||
fields=["name", "target_asset"],
|
||||
)
|
||||
for asset_capitalization in cancelled_asset_capitalizations:
|
||||
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
||||
@@ -1781,6 +1781,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
apply_price_list(item, reset_plc_conversion) {
|
||||
// We need to reset plc_conversion_rate sometimes because the call to
|
||||
// `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value
|
||||
|
||||
|
||||
if (this.frm.doc.doctype === "Material Request") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reset_plc_conversion) {
|
||||
this.frm.set_value("plc_conversion_rate", "");
|
||||
}
|
||||
@@ -1796,7 +1802,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
me.in_apply_price_list = true;
|
||||
return this.frm.call({
|
||||
method: "erpnext.stock.get_item_details.apply_price_list",
|
||||
args: { args: args },
|
||||
args: { args: args, doc: me.frm.doc },
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frappe.run_serially([
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"label": "Customer Type",
|
||||
"oldfieldname": "customer_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ class Customer(TransactionBase):
|
||||
customer_pos_id: DF.Data | None
|
||||
customer_primary_address: DF.Link | None
|
||||
customer_primary_contact: DF.Link | None
|
||||
customer_type: DF.Literal["Company", "Individual", "Proprietorship", "Partnership"]
|
||||
customer_type: DF.Literal["Company", "Individual", "Partnership"]
|
||||
default_bank_account: DF.Link | None
|
||||
default_commission_rate: DF.Float
|
||||
default_currency: DF.Link | None
|
||||
|
||||
@@ -603,6 +603,17 @@ class SalesOrder(SellingController):
|
||||
if total_picked_qty and total_qty:
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
|
||||
pick_percentage = frappe.db.get_single_value("Stock Settings", "over_picking_allowance")
|
||||
if pick_percentage:
|
||||
total_qty += flt(total_qty) * (pick_percentage / 100)
|
||||
|
||||
if total_picked_qty > total_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Total Picked Quantity {0} is more than ordered qty {1}. You can set the Over Picking Allowance in Stock Settings."
|
||||
).format(total_picked_qty, total_qty)
|
||||
)
|
||||
|
||||
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
|
||||
def set_indicator(self):
|
||||
|
||||
@@ -583,6 +583,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
new_item["serial_no"] = serial_no;
|
||||
}
|
||||
|
||||
new_item["use_serial_batch_fields"] = 1;
|
||||
if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0;
|
||||
|
||||
item_row = this.frm.add_child("items", new_item);
|
||||
|
||||
@@ -101,7 +101,8 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
|
||||
const serialized = item_row.has_serial_no;
|
||||
const batched = item_row.has_batch_no;
|
||||
const no_bundle_selected = !item_row.serial_and_batch_bundle;
|
||||
const no_bundle_selected =
|
||||
!item_row.serial_and_batch_bundle && !item_row.serial_no && !item_row.batch_no;
|
||||
|
||||
if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) {
|
||||
frappe.show_alert({
|
||||
@@ -403,6 +404,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
frappe.model.set_value(item_row.doctype, item_row.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
qty: Math.abs(r.total_qty),
|
||||
use_serial_batch_fields: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"fieldname": "auto_err_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "Daily\nWeekly"
|
||||
"options": "Daily\nWeekly\nMonthly"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -808,7 +808,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-21 17:46:25.567565",
|
||||
"modified": "2024-07-24 18:17:56.413971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
|
||||
@@ -31,7 +31,7 @@ class Company(NestedSet):
|
||||
accumulated_depreciation_account: DF.Link | None
|
||||
allow_account_creation_against_child_company: DF.Check
|
||||
asset_received_but_not_billed: DF.Link | None
|
||||
auto_err_frequency: DF.Literal["Daily", "Weekly"]
|
||||
auto_err_frequency: DF.Literal["Daily", "Weekly", "Monthly"]
|
||||
auto_exchange_rate_revaluation: DF.Check
|
||||
book_advance_payments_in_separate_party_account: DF.Check
|
||||
capital_work_in_progress_account: DF.Link | None
|
||||
|
||||
@@ -163,7 +163,7 @@ def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ def make_item_tax_template(company_name, template):
|
||||
# Ingone validations to make doctypes faster
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ def get_or_create_account(company_name, account):
|
||||
doc = frappe.get_doc(account)
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
@@ -1918,6 +1918,127 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
returned_serial_nos = get_serial_nos_from_bundle(dn_return.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(serial_nos, returned_serial_nos)
|
||||
|
||||
def test_same_posting_date_and_posting_time(self):
|
||||
item_code = make_item(
|
||||
"Test Same Posting Datetime Item",
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "SS-ART11-TESTBATCH.#####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=100,
|
||||
basic_rate=50,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
posting_date = today()
|
||||
posting_time = nowtime()
|
||||
dn1 = create_delivery_note(
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
item_code=item_code,
|
||||
rate=300,
|
||||
qty=25,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
dn2 = create_delivery_note(
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
item_code=item_code,
|
||||
rate=300,
|
||||
qty=25,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
dn3 = create_delivery_note(
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
item_code=item_code,
|
||||
rate=300,
|
||||
qty=25,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
dn4 = create_delivery_note(
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
item_code=item_code,
|
||||
rate=300,
|
||||
qty=25,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
for dn in [dn1, dn2, dn3, dn4]:
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["stock_value_difference", "actual_qty"],
|
||||
filters={"is_cancelled": 0, "voucher_no": dn.name, "docstatus": 1},
|
||||
)
|
||||
|
||||
for sle in sles:
|
||||
self.assertEqual(sle.actual_qty, 25.0 * -1)
|
||||
self.assertEqual(sle.stock_value_difference, 25.0 * 50 * -1)
|
||||
|
||||
dn5 = create_delivery_note(
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
item_code=item_code,
|
||||
rate=300,
|
||||
qty=25,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, dn5.submit)
|
||||
|
||||
def test_warranty_expiry_date_for_serial_item(self):
|
||||
item_code = make_item(
|
||||
"Test Warranty Expiry Date Item",
|
||||
properties={
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TWE.#####",
|
||||
"is_stock_item": 1,
|
||||
"warranty_period": 100,
|
||||
},
|
||||
).name
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=2,
|
||||
basic_rate=50,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
|
||||
serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
create_delivery_note(
|
||||
item_code=item_code,
|
||||
qty=2,
|
||||
rate=300,
|
||||
use_serial_batch_fields=0,
|
||||
serial_no=serial_nos,
|
||||
)
|
||||
|
||||
for row in serial_nos:
|
||||
sn = frappe.get_doc("Serial No", row)
|
||||
self.assertEqual(getdate(sn.warranty_expiry_date), getdate(add_days(nowdate(), 100)))
|
||||
self.assertEqual(sn.status, "Delivered")
|
||||
self.assertEqual(sn.warranty_period, 100)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -587,6 +587,14 @@ $.extend(erpnext.item, {
|
||||
me.multiple_variant_dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Attribute Values"),
|
||||
fields: [
|
||||
frm.doc.image
|
||||
? {
|
||||
fieldtype: "Check",
|
||||
label: __("Create a variant with the template image."),
|
||||
fieldname: "use_template_image",
|
||||
default: 0,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "help",
|
||||
@@ -594,11 +602,14 @@ $.extend(erpnext.item, {
|
||||
${__("Select at least one value from each of the attributes.")}
|
||||
</label>`,
|
||||
},
|
||||
].concat(fields),
|
||||
]
|
||||
.concat(fields)
|
||||
.filter(Boolean),
|
||||
});
|
||||
|
||||
me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => {
|
||||
let selected_attributes = get_selected_attributes();
|
||||
let use_template_image = me.multiple_variant_dialog.get_value("use_template_image");
|
||||
|
||||
me.multiple_variant_dialog.hide();
|
||||
frappe.call({
|
||||
@@ -606,6 +617,7 @@ $.extend(erpnext.item, {
|
||||
args: {
|
||||
item: frm.doc.name,
|
||||
args: selected_attributes,
|
||||
use_template_image: use_template_image,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message === "queued") {
|
||||
@@ -720,6 +732,15 @@ $.extend(erpnext.item, {
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.image) {
|
||||
fields.push({
|
||||
fieldtype: "Check",
|
||||
label: __("Create a variant with the template image."),
|
||||
fieldname: "use_template_image",
|
||||
default: 0,
|
||||
});
|
||||
}
|
||||
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Create Variant"),
|
||||
fields: fields,
|
||||
@@ -761,6 +782,7 @@ $.extend(erpnext.item, {
|
||||
args: {
|
||||
item: frm.doc.name,
|
||||
args: d.get_values(),
|
||||
use_template_image: args.use_template_image,
|
||||
},
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
|
||||
@@ -1174,3 +1174,34 @@ class TestPickList(FrappeTestCase):
|
||||
row.qty = row.qty + 10
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pl.save)
|
||||
|
||||
def test_over_allowance_picking(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Over Allowance Picking Item",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl_doc = create_pick_list(so.name)
|
||||
pl_doc.save()
|
||||
self.assertEqual(pl_doc.locations[0].qty, 10)
|
||||
|
||||
pl_doc.locations[0].qty = 15
|
||||
pl_doc.locations[0].stock_qty = 15
|
||||
pl_doc.save()
|
||||
|
||||
self.assertEqual(pl_doc.locations[0].qty, 15)
|
||||
self.assertRaises(frappe.ValidationError, pl_doc.submit)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 50)
|
||||
|
||||
pl_doc.reload()
|
||||
pl_doc.submit()
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0)
|
||||
|
||||
@@ -1886,9 +1886,19 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
rate=100,
|
||||
rejected_qty=2,
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{"item_code": item_code, "qty": 2, "rate": 100, "warehouse": warehouse, "rejected_qty": 0},
|
||||
)
|
||||
pr.save()
|
||||
pr.submit()
|
||||
self.assertEqual(len(pr.items), 2)
|
||||
|
||||
pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
|
||||
self.assertEqual(len(pr_return.items), 1)
|
||||
self.assertEqual(pr_return.items[0].warehouse, rejected_warehouse)
|
||||
self.assertEqual(pr_return.items[0].qty, 2.0 * -1)
|
||||
self.assertEqual(pr_return.items[0].rejected_qty, 0.0)
|
||||
@@ -3350,6 +3360,122 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
self.assertEqual(pr.grand_total, 0.0)
|
||||
self.assertEqual(pr.status, "Completed")
|
||||
|
||||
def test_internal_transfer_for_batch_items_with_cancel(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
batch_item_doc = make_item(
|
||||
"_Test Batch Item For Stock Transfer Cancel Case",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-CANBIFST-.####"},
|
||||
)
|
||||
|
||||
serial_item_doc = make_item(
|
||||
"_Test Serial No Item For Stock Transfer Cancel Case",
|
||||
{"has_serial_no": 1, "serial_no_series": "USBF-BT-CANBIFST-.####"},
|
||||
)
|
||||
|
||||
inward_entry = make_purchase_receipt(
|
||||
item_code=batch_item_doc.name,
|
||||
qty=10,
|
||||
rate=150,
|
||||
warehouse="Stores - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
use_serial_batch_fields=0,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
inward_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item_doc.name,
|
||||
"qty": 15,
|
||||
"rate": 250,
|
||||
"item_name": serial_item_doc.item_name,
|
||||
"conversion_factor": 1.0,
|
||||
"uom": serial_item_doc.stock_uom,
|
||||
"stock_uom": serial_item_doc.stock_uom,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"use_serial_batch_fields": 0,
|
||||
},
|
||||
)
|
||||
|
||||
inward_entry.submit()
|
||||
inward_entry.reload()
|
||||
|
||||
for row in inward_entry.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_dn = create_delivery_note(
|
||||
item_code=inward_entry.items[0].item_code,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=500,
|
||||
warehouse="Stores - TCP1",
|
||||
target_warehouse="Work In Progress - TCP1",
|
||||
batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
|
||||
use_serial_batch_fields=0,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
inter_transfer_dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item_doc.name,
|
||||
"qty": 15,
|
||||
"rate": 350,
|
||||
"item_name": serial_item_doc.item_name,
|
||||
"conversion_factor": 1.0,
|
||||
"uom": serial_item_doc.stock_uom,
|
||||
"stock_uom": serial_item_doc.stock_uom,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"target_warehouse": "Work In Progress - TCP1",
|
||||
"serial_no": "\n".join(
|
||||
get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
|
||||
),
|
||||
"use_serial_batch_fields": 0,
|
||||
},
|
||||
)
|
||||
|
||||
inter_transfer_dn.submit()
|
||||
inter_transfer_dn.reload()
|
||||
for row in inter_transfer_dn.items:
|
||||
if row.item_code == batch_item_doc.name:
|
||||
self.assertEqual(row.rate, 150.0)
|
||||
else:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
|
||||
for row in inter_transfer_pr.items:
|
||||
row.from_warehouse = "Work In Progress - TCP1"
|
||||
row.warehouse = "Stores - TCP1"
|
||||
inter_transfer_pr.submit()
|
||||
|
||||
for row in inter_transfer_pr.items:
|
||||
if row.item_code == batch_item_doc.name:
|
||||
self.assertEqual(row.rate, 150.0)
|
||||
else:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_pr.cancel()
|
||||
inter_transfer_dn.cancel()
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -242,6 +242,9 @@ class QualityInspection(Document):
|
||||
# numeric readings
|
||||
for i in range(1, 11):
|
||||
field = "reading_" + str(i)
|
||||
if reading.get(field) is None:
|
||||
continue
|
||||
|
||||
data[field] = parse_float(reading.get(field))
|
||||
data["mean"] = self.calculate_mean(reading)
|
||||
|
||||
|
||||
@@ -289,6 +289,11 @@ def repost(doc):
|
||||
if isinstance(message, dict):
|
||||
message = message.get("message")
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and "timeout" in traceback.lower():
|
||||
status = "In Progress"
|
||||
|
||||
if traceback:
|
||||
message += "<br><br>" + "<b>Traceback:</b> <br>" + traceback
|
||||
|
||||
@@ -297,7 +302,7 @@ def repost(doc):
|
||||
doc.name,
|
||||
{
|
||||
"error_log": message,
|
||||
"status": "Failed",
|
||||
"status": status,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -92,8 +92,10 @@ class SerialandBatchBundle(Document):
|
||||
if self.type_of_transaction == "Maintenance":
|
||||
return
|
||||
|
||||
self.validate_serial_nos_duplicate()
|
||||
self.check_future_entries_exists()
|
||||
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
||||
self.validate_serial_nos_duplicate()
|
||||
self.check_future_entries_exists()
|
||||
|
||||
self.set_is_outward()
|
||||
self.calculate_total_qty()
|
||||
self.set_warehouse()
|
||||
@@ -300,6 +302,7 @@ class SerialandBatchBundle(Document):
|
||||
"batch_nos": {row.batch_no: row for row in self.entries if row.batch_no},
|
||||
"voucher_type": self.voucher_type,
|
||||
"voucher_detail_no": self.voucher_detail_no,
|
||||
"creation": self.creation,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -339,6 +342,9 @@ class SerialandBatchBundle(Document):
|
||||
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
|
||||
|
||||
for d in self.entries:
|
||||
if (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
||||
continue
|
||||
|
||||
d.incoming_rate = flt(rate, precision)
|
||||
if d.qty:
|
||||
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
||||
@@ -392,32 +398,6 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
self.calculate_qty_and_amount(save=True)
|
||||
self.validate_quantity(row, qty_field=qty_field)
|
||||
self.set_warranty_expiry_date()
|
||||
|
||||
def set_warranty_expiry_date(self):
|
||||
if self.type_of_transaction != "Outward":
|
||||
return
|
||||
|
||||
if not (self.docstatus == 1 and self.voucher_type == "Delivery Note" and self.has_serial_no):
|
||||
return
|
||||
|
||||
warranty_period = frappe.get_cached_value("Item", self.item_code, "warranty_period")
|
||||
|
||||
if not warranty_period:
|
||||
return
|
||||
|
||||
warranty_expiry_date = add_days(self.posting_date, cint(warranty_period))
|
||||
|
||||
serial_nos = self.get_serial_nos()
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
(
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warranty_expiry_date, warranty_expiry_date)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
|
||||
def validate_voucher_no(self):
|
||||
if not (self.voucher_type and self.voucher_no):
|
||||
@@ -866,6 +846,9 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_serial_nos_inventory()
|
||||
|
||||
def set_purchase_document_no(self):
|
||||
if self.flags.ignore_validate_serial_batch:
|
||||
return
|
||||
|
||||
if not self.has_serial_no:
|
||||
return
|
||||
|
||||
@@ -934,6 +917,9 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_voucher_no_docstatus()
|
||||
|
||||
def validate_voucher_no_docstatus(self):
|
||||
if self.voucher_type == "POS Invoice":
|
||||
return
|
||||
|
||||
if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1:
|
||||
msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
|
||||
is in submitted state, please cancel it first"""
|
||||
@@ -1097,7 +1083,9 @@ def create_serial_nos(item_code, serial_nos):
|
||||
|
||||
|
||||
def make_serial_nos(item_code, serial_nos):
|
||||
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
|
||||
item = frappe.get_cached_value(
|
||||
"Item", item_code, ["description", "item_code", "item_name", "warranty_period"], as_dict=1
|
||||
)
|
||||
|
||||
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
|
||||
existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
|
||||
@@ -1122,6 +1110,7 @@ def make_serial_nos(item_code, serial_nos):
|
||||
item.item_code,
|
||||
item.item_name,
|
||||
item.description,
|
||||
item.warranty_period or 0,
|
||||
"Inactive",
|
||||
)
|
||||
)
|
||||
@@ -1136,6 +1125,7 @@ def make_serial_nos(item_code, serial_nos):
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
"warranty_period",
|
||||
"status",
|
||||
]
|
||||
|
||||
@@ -1717,6 +1707,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
|
||||
"`tabPOS Invoice Item`.warehouse",
|
||||
"`tabPOS Invoice Item`.name as child_docname",
|
||||
"`tabPOS Invoice`.name as parent_docname",
|
||||
"`tabPOS Invoice Item`.use_serial_batch_fields",
|
||||
"`tabPOS Invoice Item`.serial_and_batch_bundle",
|
||||
],
|
||||
filters=[
|
||||
@@ -1730,7 +1721,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
|
||||
ids = [
|
||||
pos_invoice.serial_and_batch_bundle
|
||||
for pos_invoice in pos_invoices
|
||||
if pos_invoice.serial_and_batch_bundle
|
||||
if pos_invoice.serial_and_batch_bundle and not pos_invoice.use_serial_batch_fields
|
||||
]
|
||||
|
||||
if ids:
|
||||
@@ -2179,6 +2170,8 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
||||
|
||||
|
||||
def get_stock_ledgers_batches(kwargs):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_table = frappe.qb.DocType("Batch")
|
||||
|
||||
@@ -2205,6 +2198,19 @@ def get_stock_ledgers_batches(kwargs):
|
||||
else:
|
||||
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
if kwargs.get("ignore_voucher_nos"):
|
||||
query = query.where(stock_ledger_entry.voucher_no.notin(kwargs.get("ignore_voucher_nos")))
|
||||
|
||||
if kwargs.based_on == "LIFO":
|
||||
query = query.orderby(batch_table.creation, order=frappe.qb.desc)
|
||||
elif kwargs.based_on == "Expiry":
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
"job_card",
|
||||
"amended_from",
|
||||
"credit_note",
|
||||
"is_return"
|
||||
"is_return",
|
||||
"tab_connections"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -683,6 +684,12 @@
|
||||
"label": "Asset Repair",
|
||||
"options": "Asset Repair",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_connections",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
|
||||
@@ -2354,17 +2354,22 @@ class StockEntry(StockController):
|
||||
return [d.item_code for d in job_card_items]
|
||||
|
||||
def add_to_stock_entry_detail(self, item_dict, bom_no=None):
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
for d in item_dict:
|
||||
item_row = item_dict[d]
|
||||
stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
|
||||
|
||||
child_qty = flt(item_row["qty"], precision)
|
||||
if not self.is_return and child_qty <= 0:
|
||||
continue
|
||||
|
||||
se_child = self.append("items")
|
||||
stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
|
||||
se_child.s_warehouse = item_row.get("from_warehouse")
|
||||
se_child.t_warehouse = item_row.get("to_warehouse")
|
||||
se_child.item_code = item_row.get("item_code") or cstr(d)
|
||||
se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom
|
||||
se_child.stock_uom = stock_uom
|
||||
se_child.qty = flt(item_row["qty"], se_child.precision("qty"))
|
||||
se_child.qty = child_qty
|
||||
se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0)
|
||||
se_child.subcontracted_item = item_row.get("main_item_code")
|
||||
se_child.cost_center = item_row.get("cost_center") or get_default_cost_center(
|
||||
@@ -2756,7 +2761,7 @@ def make_stock_in_entry(source_name, target_doc=None):
|
||||
"batch_no": "batch_no",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01,
|
||||
"condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.00001,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
26
erpnext/stock/doctype/stock_entry/stock_entry_dashboard.py
Normal file
26
erpnext/stock/doctype/stock_entry/stock_entry_dashboard.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
# Todo: non_standard_fieldnames is to be decided
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "stock_entry",
|
||||
"non_standard_fieldnames": {
|
||||
# "DocType Name": "Reference field name",
|
||||
},
|
||||
"internal_links": {
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
"Subcontracting Order": ["items", "subcontracting_order"],
|
||||
"Subcontracting Receipt": ["items", "subcontracting_receipt"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": [
|
||||
"Purchase Order",
|
||||
"Subcontracting Order",
|
||||
"Subcontracting Receipt",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
|
||||
get_available_serial_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.utils import get_stock_balance
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
class OpeningEntryAccountError(frappe.ValidationError):
|
||||
@@ -952,14 +952,21 @@ class StockReconciliation(StockController):
|
||||
precesion = row.precision("current_qty")
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
if not row.serial_no:
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
val_rate = get_incoming_rate(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"qty": current_qty * -1,
|
||||
"serial_and_batch_bundle": row.current_serial_and_batch_bundle,
|
||||
"batch_no": row.batch_no,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# ERPNext - web based ERP (http://erpnext.com)
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
@@ -1182,6 +1183,98 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
self.assertAlmostEqual(row.incoming_rate, 1000.00)
|
||||
self.assertEqual(row.serial_no, serial_nos[row.idx - 1])
|
||||
|
||||
def test_stock_reco_with_legacy_batch(self):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
batch_item_code = self.make_item(
|
||||
"Test Batch Item Legacy Batch 1",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BH1-NRALL-S-.###",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
|
||||
batch_id = "BH1-NRALL-S-0001"
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": batch_item_code,
|
||||
"use_batchwise_valuation": 0,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
self.assertTrue(batch_doc.use_batchwise_valuation)
|
||||
|
||||
stock_queue = []
|
||||
qty_after_transaction = 0
|
||||
balance_value = 0
|
||||
i = 0
|
||||
for qty, valuation in {10: 100, 20: 200}.items():
|
||||
i += 1
|
||||
stock_queue.append([qty, valuation])
|
||||
qty_after_transaction += qty
|
||||
balance_value += qty_after_transaction * valuation
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Ledger Entry",
|
||||
"posting_date": add_days(nowdate(), -2 * i),
|
||||
"posting_time": nowtime(),
|
||||
"batch_no": batch_id,
|
||||
"incoming_rate": valuation,
|
||||
"qty_after_transaction": qty_after_transaction,
|
||||
"stock_value_difference": valuation * qty,
|
||||
"balance_value": balance_value,
|
||||
"valuation_rate": balance_value / qty_after_transaction,
|
||||
"actual_qty": qty,
|
||||
"item_code": batch_item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_queue": json.dumps(stock_queue),
|
||||
}
|
||||
)
|
||||
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.submit()
|
||||
doc.reload()
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
batch_doc = frappe.get_doc("Batch", batch_id)
|
||||
|
||||
qty = get_batch_qty(batch_id, warehouse, batch_item_code)
|
||||
self.assertEqual(qty, 30)
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=batch_item_code,
|
||||
posting_date=add_days(nowdate(), -3),
|
||||
posting_time=nowtime(),
|
||||
warehouse=warehouse,
|
||||
qty=100,
|
||||
rate=1000,
|
||||
reconcile_all_serial_batch=0,
|
||||
batch_no=batch_id,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
self.assertEqual(sr.items[0].current_qty, 20)
|
||||
self.assertEqual(sr.items[0].qty, 100)
|
||||
|
||||
qty = get_batch_qty(batch_id, warehouse, batch_item_code)
|
||||
self.assertEqual(qty, 110)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"section_break_9",
|
||||
"over_delivery_receipt_allowance",
|
||||
"mr_qty_allowance",
|
||||
"over_picking_allowance",
|
||||
"column_break_121",
|
||||
"role_allowed_to_over_deliver_receive",
|
||||
"allow_negative_stock",
|
||||
@@ -433,7 +434,6 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "use_serial_batch_fields",
|
||||
"description": "If enabled, do not update serial / batch values in the stock transactions on creation of auto Serial \n / Batch Bundle. ",
|
||||
"fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle",
|
||||
"fieldtype": "Check",
|
||||
@@ -446,6 +446,12 @@
|
||||
"fieldname": "do_not_use_batchwise_valuation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do Not Use Batch-wise Valuation"
|
||||
},
|
||||
{
|
||||
"description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.",
|
||||
"fieldname": "over_picking_allowance",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Over Picking Allowance"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -453,7 +459,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-04 12:45:09.811280",
|
||||
"modified": "2024-07-29 14:55:19.093508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
||||
@@ -47,6 +47,7 @@ class StockSettings(Document):
|
||||
mr_qty_allowance: DF.Float
|
||||
naming_series_prefix: DF.Data | None
|
||||
over_delivery_receipt_allowance: DF.Float
|
||||
over_picking_allowance: DF.Percent
|
||||
pick_serial_and_batch_based_on: DF.Literal["FIFO", "LIFO", "Expiry"]
|
||||
reorder_email_notify: DF.Check
|
||||
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
|
||||
|
||||
@@ -40,32 +40,40 @@ frappe.ui.form.on("Warehouse", {
|
||||
if (!frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
|
||||
let enable_toggle = frm.doc.disabled ? "Enable" : "Disable";
|
||||
frm.add_custom_button(__(enable_toggle), () => {
|
||||
frm.set_value("disabled", 1 - frm.doc.disabled);
|
||||
frm.save();
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Stock Balance"), function () {
|
||||
frappe.set_route("query-report", "Stock Balance", {
|
||||
warehouse: frm.doc.name,
|
||||
company: frm.doc.company,
|
||||
if (frm.has_perm("write")) {
|
||||
let enable_toggle = frm.doc.disabled ? "Enable" : "Disable";
|
||||
frm.add_custom_button(__(enable_toggle), () => {
|
||||
frm.set_value("disabled", 1 - frm.doc.disabled);
|
||||
frm.save();
|
||||
});
|
||||
});
|
||||
|
||||
frm.add_custom_button(
|
||||
frm.doc.is_group
|
||||
? __("Convert to Ledger", null, "Warehouse")
|
||||
: __("Convert to Group", null, "Warehouse"),
|
||||
function () {
|
||||
convert_to_group_or_ledger(frm);
|
||||
}
|
||||
);
|
||||
frm.add_custom_button(
|
||||
frm.doc.is_group
|
||||
? __("Convert to Ledger", null, "Warehouse")
|
||||
: __("Convert to Group", null, "Warehouse"),
|
||||
function () {
|
||||
convert_to_group_or_ledger(frm);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ("Stock Balance" in frappe.boot.user.all_reports) {
|
||||
frm.add_custom_button(__("Stock Balance"), function () {
|
||||
frappe.set_route("query-report", "Stock Balance", {
|
||||
warehouse: frm.doc.name,
|
||||
company: frm.doc.company,
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
|
||||
if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) {
|
||||
if (
|
||||
!frm.doc.is_group &&
|
||||
frm.doc.__onload?.account &&
|
||||
"General Ledger" in frappe.boot.user.all_reports
|
||||
) {
|
||||
frm.add_custom_button(__("General Ledger", null, "Warehouse"), function () {
|
||||
frappe.route_options = {
|
||||
account: frm.doc.__onload.account,
|
||||
|
||||
@@ -1189,7 +1189,7 @@ def get_batch_qty(batch_no, warehouse, item_code):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_price_list(args, as_doc=False):
|
||||
def apply_price_list(args, as_doc=False, doc=None):
|
||||
"""Apply pricelist on a document-like dict object and return as
|
||||
{'parent': dict, 'children': list}
|
||||
|
||||
@@ -1228,7 +1228,7 @@ def apply_price_list(args, as_doc=False):
|
||||
for item in item_list:
|
||||
args_copy = frappe._dict(args.copy())
|
||||
args_copy.update(item)
|
||||
item_details = apply_price_list_on_item(args_copy)
|
||||
item_details = apply_price_list_on_item(args_copy, doc=doc)
|
||||
children.append(item_details)
|
||||
|
||||
if as_doc:
|
||||
@@ -1246,10 +1246,10 @@ def apply_price_list(args, as_doc=False):
|
||||
return {"parent": parent, "children": children}
|
||||
|
||||
|
||||
def apply_price_list_on_item(args):
|
||||
def apply_price_list_on_item(args, doc=None):
|
||||
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
|
||||
item_details = get_price_list_rate(args, item_doc)
|
||||
item_details.update(get_pricing_rule_for_item(args))
|
||||
item_details.update(get_pricing_rule_for_item(args, doc=doc))
|
||||
|
||||
return item_details
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
|
||||
frappe.query_reports["Product Bundle Balance"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "date",
|
||||
label: __("Date"),
|
||||
|
||||
@@ -224,6 +224,9 @@ def get_stock_ledger_entries(filters, items):
|
||||
.where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items)))
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(sle.company == filters.get("company"))
|
||||
|
||||
if date := filters.get("date"):
|
||||
query = query.where(sle.posting_date <= date)
|
||||
else:
|
||||
@@ -237,7 +240,7 @@ def get_stock_ledger_entries(filters, items):
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
sle.warehouse.isin(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt))
|
||||
|
||||
@@ -77,9 +77,8 @@ def get_columns(filters):
|
||||
},
|
||||
{
|
||||
"label": _("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "party_type",
|
||||
"options": "DocType",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
@@ -158,6 +157,7 @@ def get_data(filters):
|
||||
{
|
||||
"serial_no": bundle_data.get("serial_no"),
|
||||
"valuation_rate": bundle_data.get("valuation_rate"),
|
||||
"qty": args.qty,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.stock.deprecated_serial_batch import (
|
||||
@@ -110,6 +110,7 @@ class SerialBatchBundle:
|
||||
"type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
|
||||
"company": self.company,
|
||||
"is_rejected": self.is_rejected_entry(),
|
||||
"make_bundle_from_sle": 1,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
@@ -160,12 +161,13 @@ class SerialBatchBundle:
|
||||
|
||||
if msg:
|
||||
error_msg = (
|
||||
f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
|
||||
f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}"
|
||||
+ msg
|
||||
)
|
||||
frappe.throw(_(error_msg))
|
||||
|
||||
def set_serial_and_batch_bundle(self, sn_doc):
|
||||
self.sle.auto_created_serial_and_batch_bundle = 1
|
||||
self.sle.db_set({"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1})
|
||||
|
||||
if sn_doc.is_rejected:
|
||||
@@ -246,6 +248,9 @@ class SerialBatchBundle:
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def delink_serial_and_batch_bundle(self):
|
||||
if self.is_pos_transaction():
|
||||
return
|
||||
|
||||
update_values = {
|
||||
"serial_and_batch_bundle": "",
|
||||
}
|
||||
@@ -295,8 +300,22 @@ class SerialBatchBundle:
|
||||
self.cancel_serial_and_batch_bundle()
|
||||
|
||||
def cancel_serial_and_batch_bundle(self):
|
||||
if self.is_pos_transaction():
|
||||
return
|
||||
|
||||
frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
|
||||
|
||||
def is_pos_transaction(self):
|
||||
if (
|
||||
self.sle.voucher_type == "Sales Invoice"
|
||||
and self.sle.serial_and_batch_bundle
|
||||
and frappe.get_cached_value(
|
||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "voucher_type"
|
||||
)
|
||||
== "POS Invoice"
|
||||
):
|
||||
return True
|
||||
|
||||
def submit_serial_and_batch_bundle(self):
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
||||
self.validate_actual_qty(doc)
|
||||
@@ -307,6 +326,9 @@ class SerialBatchBundle:
|
||||
def set_warehouse_and_status_in_serial_nos(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
|
||||
|
||||
if self.sle.auto_created_serial_and_batch_bundle and self.sle.actual_qty > 0:
|
||||
return
|
||||
|
||||
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
|
||||
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
|
||||
serial_nos = get_parsed_serial_nos(self.sle.serial_no)
|
||||
@@ -321,7 +343,8 @@ class SerialBatchBundle:
|
||||
status = "Delivered"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
(
|
||||
|
||||
query = (
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(
|
||||
@@ -334,7 +357,19 @@ class SerialBatchBundle:
|
||||
)
|
||||
.set(sn_table.company, self.sle.company)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
)
|
||||
|
||||
if status == "Delivered":
|
||||
warranty_period = frappe.get_cached_value("Item", self.sle.item_code, "warranty_period")
|
||||
if warranty_period:
|
||||
warranty_expiry_date = add_days(self.sle.posting_date, cint(warranty_period))
|
||||
query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date)
|
||||
query = query.set(sn_table.warranty_period, warranty_period)
|
||||
else:
|
||||
query = query.set(sn_table.warranty_expiry_date, None)
|
||||
query = query.set(sn_table.warranty_period, 0)
|
||||
|
||||
query.run()
|
||||
|
||||
def set_batch_no_in_serial_nos(self):
|
||||
entries = frappe.get_all(
|
||||
@@ -599,9 +634,15 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
|
||||
timestamp_condition = ""
|
||||
if self.sle.posting_date and self.sle.posting_time:
|
||||
timestamp_condition = CombineDatetime(
|
||||
parent.posting_date, parent.posting_time
|
||||
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime(
|
||||
self.sle.posting_date, self.sle.posting_time
|
||||
)
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (
|
||||
CombineDatetime(parent.posting_date, parent.posting_time)
|
||||
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
) & (parent.creation < self.sle.creation)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
@@ -892,6 +933,10 @@ class SerialBatchCreation:
|
||||
if doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2:
|
||||
doc.voucher_no = ""
|
||||
|
||||
doc.flags.ignore_validate_serial_batch = False
|
||||
if self.get("make_bundle_from_sle") and self.type_of_transaction == "Inward":
|
||||
doc.flags.ignore_validate_serial_batch = True
|
||||
|
||||
doc.save()
|
||||
self.validate_qty(doc)
|
||||
|
||||
@@ -1084,6 +1129,10 @@ class SerialBatchCreation:
|
||||
msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
|
||||
frappe.throw(_(msg))
|
||||
|
||||
voucher_no = ""
|
||||
if self.get("voucher_no"):
|
||||
voucher_no = self.get("voucher_no")
|
||||
|
||||
for _i in range(abs(cint(self.actual_qty))):
|
||||
serial_no = make_autoname(self.serial_no_series, "Serial No")
|
||||
sr_nos.append(serial_no)
|
||||
@@ -1101,6 +1150,7 @@ class SerialBatchCreation:
|
||||
self.item_name,
|
||||
self.description,
|
||||
"Active",
|
||||
voucher_no,
|
||||
self.batch_no,
|
||||
)
|
||||
)
|
||||
@@ -1119,6 +1169,7 @@ class SerialBatchCreation:
|
||||
"item_name",
|
||||
"description",
|
||||
"status",
|
||||
"purchase_document_no",
|
||||
"batch_no",
|
||||
]
|
||||
|
||||
|
||||
@@ -267,13 +267,17 @@ def repost_future_sle(
|
||||
"posting_time": args[i].get("posting_time"),
|
||||
"creation": args[i].get("creation"),
|
||||
"distinct_item_warehouses": distinct_item_warehouses,
|
||||
"items_to_be_repost": args,
|
||||
"current_index": i,
|
||||
},
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
affected_transactions.update(obj.affected_transactions)
|
||||
|
||||
distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True
|
||||
key = (args[i].get("item_code"), args[i].get("warehouse"))
|
||||
if distinct_item_warehouses.get(key):
|
||||
distinct_item_warehouses[key].reposting_status = True
|
||||
|
||||
if obj.new_items_found:
|
||||
for _item_wh, data in distinct_item_warehouses.items():
|
||||
@@ -685,11 +689,20 @@ class update_entries_after:
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
else:
|
||||
# Check if the dependent voucher is reposted
|
||||
# If not, then do not add it to the list
|
||||
if not self.is_dependent_voucher_reposted(dependant_sle):
|
||||
return
|
||||
|
||||
existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
|
||||
|
||||
dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
|
||||
|
||||
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
|
||||
if dependent_voucher_detail_nos and dependant_sle.voucher_detail_no in set(
|
||||
dependent_voucher_detail_nos
|
||||
):
|
||||
return
|
||||
|
||||
val.sle_changed = True
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
@@ -703,6 +716,27 @@ class update_entries_after:
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
self.distinct_item_warehouses[key] = val
|
||||
|
||||
def is_dependent_voucher_reposted(self, dependant_sle) -> bool:
|
||||
# Return False if the dependent voucher is not reposted
|
||||
|
||||
if self.args.items_to_be_repost and self.args.current_index:
|
||||
index = self.args.current_index
|
||||
while index < len(self.args.items_to_be_repost):
|
||||
if (
|
||||
self.args.items_to_be_repost[index].get("item_code") == dependant_sle.item_code
|
||||
and self.args.items_to_be_repost[index].get("warehouse") == dependant_sle.warehouse
|
||||
):
|
||||
if getdate(self.args.items_to_be_repost[index].get("posting_date")) > getdate(
|
||||
dependant_sle.posting_date
|
||||
):
|
||||
self.args.items_to_be_repost[index]["posting_date"] = dependant_sle.posting_date
|
||||
|
||||
return False
|
||||
|
||||
index += 1
|
||||
|
||||
return True
|
||||
|
||||
def get_dependent_voucher_detail_nos(self, key):
|
||||
if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
|
||||
self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
|
||||
@@ -1556,9 +1590,11 @@ def get_stock_ledger_entries(
|
||||
if not previous_sle.get("posting_date"):
|
||||
previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
|
||||
else:
|
||||
previous_sle["posting_datetime"] = get_combine_datetime(
|
||||
previous_sle["posting_date"], previous_sle["posting_time"]
|
||||
)
|
||||
posting_time = previous_sle.get("posting_time")
|
||||
if not posting_time:
|
||||
posting_time = "00:00:00"
|
||||
|
||||
previous_sle["posting_datetime"] = get_combine_datetime(previous_sle["posting_date"], posting_time)
|
||||
|
||||
if operator in (">", "<=") and previous_sle.get("name"):
|
||||
conditions += " and name!=%(name)s"
|
||||
|
||||
@@ -200,7 +200,7 @@ def get_bin(item_code, warehouse):
|
||||
if not bin:
|
||||
bin_obj = _create_bin(item_code, warehouse)
|
||||
else:
|
||||
bin_obj = frappe.get_doc("Bin", bin, for_update=True)
|
||||
bin_obj = frappe.get_doc("Bin", bin)
|
||||
bin_obj.flags.ignore_permissions = True
|
||||
return bin_obj
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
setup: (frm) => {
|
||||
frm.get_field("items").grid.cannot_add_rows = true;
|
||||
frm.get_field("items").grid.only_sortable();
|
||||
frm.trigger("set_queries");
|
||||
|
||||
frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange"));
|
||||
|
||||
@@ -31,6 +32,22 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", "items", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("set_warehouse", () => {
|
||||
return {
|
||||
filters: {
|
||||
@@ -77,6 +94,17 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
});
|
||||
},
|
||||
|
||||
set_queries: (frm) => {
|
||||
frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||
frm.set_query("supplier_address", erpnext.queries.address_query);
|
||||
|
||||
frm.set_query("billing_address", erpnext.queries.company_address_query);
|
||||
|
||||
frm.set_query("shipping_address", () => {
|
||||
return erpnext.queries.company_address_query(frm.doc);
|
||||
});
|
||||
},
|
||||
|
||||
onload: (frm) => {
|
||||
if (!frm.doc.transaction_date) {
|
||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||
@@ -100,6 +128,8 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||
|
||||
if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
|
||||
if (frm.doc.status == "Closed") {
|
||||
frm.add_custom_button(
|
||||
|
||||
@@ -23,18 +23,6 @@
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"address_and_contact_section",
|
||||
"supplier_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"column_break_19",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"section_break_24",
|
||||
"column_break_25",
|
||||
"set_warehouse",
|
||||
@@ -48,10 +36,23 @@
|
||||
"raw_materials_supplied_section",
|
||||
"set_reserve_warehouse",
|
||||
"supplied_items",
|
||||
"additional_costs_section",
|
||||
"tab_address_and_contact",
|
||||
"supplier_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"column_break_19",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"tab_additional_costs",
|
||||
"distribute_additional_costs_based_on",
|
||||
"additional_costs",
|
||||
"total_additional_costs",
|
||||
"tab_other_info",
|
||||
"order_status_section",
|
||||
"status",
|
||||
"column_break_39",
|
||||
@@ -59,7 +60,8 @@
|
||||
"printing_settings_section",
|
||||
"select_print_heading",
|
||||
"column_break_43",
|
||||
"letter_head"
|
||||
"letter_head",
|
||||
"tab_connections"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -95,7 +97,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Supplier",
|
||||
"label": "Job Worker",
|
||||
"options": "Supplier",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
@@ -107,7 +109,7 @@
|
||||
"fieldname": "supplier_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Supplier Name",
|
||||
"label": "Job Worker Name",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -115,7 +117,7 @@
|
||||
"depends_on": "supplier",
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Warehouse",
|
||||
"label": "Job Worker Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -166,9 +168,8 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldname": "tab_address_and_contact",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
@@ -176,14 +177,14 @@
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "supplier_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Address",
|
||||
"label": "Job Worker Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Supplier Address Details",
|
||||
"label": "Job Worker Address Details",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -191,7 +192,7 @@
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Contact",
|
||||
"label": "Job Worker Contact",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -337,11 +338,9 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "total_additional_costs",
|
||||
"depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)",
|
||||
"fieldname": "additional_costs_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldname": "tab_additional_costs",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Additional Costs"
|
||||
},
|
||||
{
|
||||
@@ -449,6 +448,17 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_other_info",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Other Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_connections",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
|
||||
@@ -28,6 +28,8 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frm.add_custom_button(
|
||||
__("Stock Ledger"),
|
||||
@@ -165,6 +167,15 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||
frm.set_query("supplier_address", erpnext.queries.address_query);
|
||||
|
||||
frm.set_query("billing_address", erpnext.queries.company_address_query);
|
||||
|
||||
frm.set_query("shipping_address", () => {
|
||||
return erpnext.queries.company_address_query(frm.doc);
|
||||
});
|
||||
|
||||
frm.set_query("rejected_warehouse", () => {
|
||||
return {
|
||||
filters: {
|
||||
@@ -174,6 +185,22 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", "items", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("supplier_warehouse", () => {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -23,18 +23,6 @@
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_addresses",
|
||||
"supplier_address",
|
||||
"contact_person",
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"col_break_address",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"sec_warehouse",
|
||||
"set_warehouse",
|
||||
"rejected_warehouse",
|
||||
@@ -53,23 +41,36 @@
|
||||
"get_current_stock",
|
||||
"raw_material_details",
|
||||
"supplied_items",
|
||||
"additional_costs_section",
|
||||
"distribute_additional_costs_based_on",
|
||||
"additional_costs",
|
||||
"total_additional_costs",
|
||||
"section_break_46",
|
||||
"in_words",
|
||||
"bill_no",
|
||||
"bill_date",
|
||||
"tab_addresses",
|
||||
"supplier_address",
|
||||
"contact_person",
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"col_break_address",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"tab_additional_costs",
|
||||
"distribute_additional_costs_based_on",
|
||||
"additional_costs",
|
||||
"total_additional_costs",
|
||||
"tab_other_info",
|
||||
"more_info",
|
||||
"status",
|
||||
"column_break_39",
|
||||
"per_returned",
|
||||
"section_break_47",
|
||||
"amended_from",
|
||||
"range",
|
||||
"column_break4",
|
||||
"represents_company",
|
||||
"order_status_section",
|
||||
"status",
|
||||
"column_break_39",
|
||||
"per_returned",
|
||||
"subscription_detail",
|
||||
"auto_repeat",
|
||||
"printing_settings",
|
||||
@@ -84,7 +85,8 @@
|
||||
"transporter_name",
|
||||
"column_break5",
|
||||
"lr_no",
|
||||
"lr_date"
|
||||
"lr_date",
|
||||
"tab_connections"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -112,7 +114,7 @@
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"label": "Supplier",
|
||||
"label": "Job Worker",
|
||||
"options": "Supplier",
|
||||
"print_hide": 1,
|
||||
"print_width": "150px",
|
||||
@@ -127,7 +129,7 @@
|
||||
"fieldname": "supplier_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Supplier Name",
|
||||
"label": "Job Worker Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -174,15 +176,14 @@
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_addresses",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldname": "tab_addresses",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address and Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Select Supplier Address",
|
||||
"label": "Select Job Worker Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -269,7 +270,7 @@
|
||||
{
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Warehouse",
|
||||
"label": "Job Worker Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
@@ -414,6 +415,7 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "subscription_detail",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat Detail"
|
||||
@@ -571,10 +573,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_47",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
@@ -598,11 +596,9 @@
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "total_additional_costs",
|
||||
"depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)",
|
||||
"fieldname": "additional_costs_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldname": "tab_additional_costs",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Additional Costs"
|
||||
},
|
||||
{
|
||||
@@ -643,7 +639,7 @@
|
||||
{
|
||||
"fieldname": "supplier_delivery_note",
|
||||
"fieldtype": "Data",
|
||||
"label": "Supplier Delivery Note"
|
||||
"label": "Job Worker Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "raw_materials_consumed_section",
|
||||
@@ -658,6 +654,23 @@
|
||||
{
|
||||
"fieldname": "column_break_uinr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_other_info",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Other Info"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "order_status_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Order Status"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_connections",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
|
||||
Reference in New Issue
Block a user