Merge pull request #42541 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-07-31 11:32:35 +05:30
committed by GitHub
45 changed files with 556 additions and 320 deletions

View File

@@ -73,7 +73,9 @@
"remarks_section", "remarks_section",
"general_ledger_remarks_length", "general_ledger_remarks_length",
"column_break_lvjk", "column_break_lvjk",
"receivable_payable_remarks_length" "receivable_payable_remarks_length",
"payment_request_settings",
"create_pr_in_draft_status"
], ],
"fields": [ "fields": [
{ {
@@ -475,6 +477,18 @@
"fieldname": "calculate_depr_using_total_days", "fieldname": "calculate_depr_using_total_days",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Calculate daily depreciation using total days in depreciation period" "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", "icon": "icon-cog",
@@ -482,7 +496,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-07-12 00:24:20.957726", "modified": "2024-07-26 06:48:52.714630",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -35,6 +35,7 @@ class AccountsSettings(Document):
book_tax_discount_loss: DF.Check book_tax_discount_loss: DF.Check
calculate_depr_using_total_days: DF.Check calculate_depr_using_total_days: DF.Check
check_supplier_invoice_uniqueness: DF.Check check_supplier_invoice_uniqueness: DF.Check
create_pr_in_draft_status: DF.Check
credit_controller: DF.Link | None credit_controller: DF.Link | None
delete_linked_ledger_entries: DF.Check delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]

View File

@@ -25,30 +25,6 @@ frappe.ui.form.on("Journal Entry", {
refresh: function (frm) { refresh: function (frm) {
erpnext.toggle_naming_series(); 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) { if (frm.doc.docstatus > 0) {
frm.add_custom_button( frm.add_custom_button(
__("Ledger"), __("Ledger"),

View File

@@ -64,8 +64,7 @@
"stock_entry", "stock_entry",
"subscription_section", "subscription_section",
"auto_repeat", "auto_repeat",
"amended_from", "amended_from"
"repost_required"
], ],
"fields": [ "fields": [
{ {
@@ -544,15 +543,6 @@
"label": "Is System Generated", "label": "Is System Generated",
"no_copy": 1, "no_copy": 1,
"read_only": 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", "icon": "fa fa-file-text",
@@ -567,7 +557,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2023-11-23 12:11:04.128015", "modified": "2024-07-18 15:32:29.413598",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -47,9 +47,7 @@ class JournalEntry(AccountsController):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import ( from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
JournalEntryAccount,
)
accounts: DF.Table[JournalEntryAccount] accounts: DF.Table[JournalEntryAccount]
amended_from: DF.Link | None amended_from: DF.Link | None
@@ -197,14 +195,10 @@ class JournalEntry(AccountsController):
self.update_booked_depreciation() self.update_booked_depreciation()
def on_update_after_submit(self): 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": []})
self.needs_repost = self.check_if_fields_updated( if self.needs_repost:
fields_to_check=[], child_tables={"accounts": []} self.validate_for_repost()
) self.repost_accounting_entries()
if self.needs_repost:
self.validate_for_repost()
self.db_set("repost_required", self.needs_repost)
self.repost_accounting_entries()
def on_cancel(self): def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller # References for this Journal are removed on the `on_cancel` event in accounts_controller

View File

@@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", {
// payment Entry // payment Entry
if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") { 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"); frm.trigger("make_payment_records");
}); });
} }

View File

@@ -500,7 +500,8 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart" or args.mute_email: if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True 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: if args.submit_doc:
pr.submit() pr.submit()

View File

@@ -77,31 +77,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); 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.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0) {
if (doc.on_hold) { if (doc.on_hold) {
this.frm.add_custom_button( this.frm.add_custom_button(

View File

@@ -170,7 +170,6 @@
"against_expense_account", "against_expense_account",
"column_break_63", "column_break_63",
"unrealized_profit_loss_account", "unrealized_profit_loss_account",
"repost_required",
"subscription_section", "subscription_section",
"subscription", "subscription",
"auto_repeat", "auto_repeat",
@@ -364,7 +363,8 @@
"description": "Once set, this invoice will be on hold till the set date", "description": "Once set, this invoice will be on hold till the set date",
"fieldname": "release_date", "fieldname": "release_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Release Date" "label": "Release Date",
"search_index": 1
}, },
{ {
"fieldname": "cb_17", "fieldname": "cb_17",
@@ -1603,15 +1603,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Company Default Round Off Cost Center" "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", "default": "0",
"fieldname": "use_transaction_date_exchange_rate", "fieldname": "use_transaction_date_exchange_rate",
@@ -1639,7 +1630,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-04-11 11:28:42.802211", "modified": "2024-07-25 19:42:36.931278",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -159,7 +159,6 @@ class PurchaseInvoice(BuyingController):
rejected_warehouse: DF.Link | None rejected_warehouse: DF.Link | None
release_date: DF.Date | None release_date: DF.Date | None
remarks: DF.SmallText | None remarks: DF.SmallText | None
repost_required: DF.Check
represents_company: DF.Link | None represents_company: DF.Link | None
return_against: DF.Link | None return_against: DF.Link | None
rounded_total: DF.Currency rounded_total: DF.Currency
@@ -796,19 +795,17 @@ class PurchaseInvoice(BuyingController):
self.process_common_party_accounting() self.process_common_party_accounting()
def on_update_after_submit(self): def on_update_after_submit(self):
if hasattr(self, "repost_required"): fields_to_check = [
fields_to_check = [ "cash_bank_account",
"cash_bank_account", "write_off_account",
"write_off_account", "unrealized_profit_loss_account",
"unrealized_profit_loss_account", "is_opening",
"is_opening", ]
] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) if self.needs_repost:
if self.needs_repost: self.validate_for_repost()
self.validate_for_repost() self.repost_accounting_entries()
self.db_set("repost_required", self.needs_repost)
self.repost_accounting_entries()
def make_gl_entries(self, gl_entries=None, from_repost=False): 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" update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
@@ -1705,6 +1702,9 @@ class PurchaseInvoice(BuyingController):
self.db_set("release_date", None) self.db_set("release_date", None)
def set_tax_withholding(self): def set_tax_withholding(self):
self.set("advance_tax", [])
self.set("tax_withheld_vouchers", [])
if not self.apply_tds: if not self.apply_tds:
return return
@@ -1746,8 +1746,6 @@ class PurchaseInvoice(BuyingController):
self.remove(d) self.remove(d)
## Add pending vouchers on which tax was withheld ## Add pending vouchers on which tax was withheld
self.set("tax_withheld_vouchers", [])
for voucher_no, voucher_details in voucher_wise_amount.items(): for voucher_no, voucher_details in voucher_wise_amount.items():
self.append( self.append(
"tax_withheld_vouchers", "tax_withheld_vouchers",
@@ -1762,7 +1760,6 @@ class PurchaseInvoice(BuyingController):
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
def allocate_advance_tds(self, tax_withholding_details, advance_taxes): def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
self.set("advance_tax", [])
for tax in advance_taxes: for tax in advance_taxes:
allocated_amount = 0 allocated_amount = 0
pending_amount = flt(tax.tax_amount - tax.allocated_amount) pending_amount = flt(tax.tax_amount - tax.allocated_amount)

View File

@@ -2014,8 +2014,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
["Service - _TC", 1000, 0.0, nowdate()], ["Service - _TC", 1000, 0.0, nowdate()],
] ]
check_gl_entries(self, pi.name, expected_gle, 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}) @change_settings("Buying Settings", {"supplier_group": None})
def test_purchase_invoice_without_supplier_group(self): def test_purchase_invoice_without_supplier_group(self):

View File

@@ -68,31 +68,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); 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) { if (this.frm.doc.is_return) {
this.frm.return_print_format = "Sales Invoice Return"; this.frm.return_print_format = "Sales Invoice Return";
} }
@@ -502,11 +477,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
frappe.msgprint(__("Please specify Company to proceed")); frappe.msgprint(__("Please specify Company to proceed"));
} else { } else {
var me = this; var me = this;
const for_validate = me.frm.doc.is_return ? true : false;
return this.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: me.frm.doc,
method: "set_missing_values", method: "set_missing_values",
args: { args: {
for_validate: true, for_validate: for_validate,
}, },
callback: function (r) { callback: function (r) {
if (!r.exc) { if (!r.exc) {

View File

@@ -213,7 +213,6 @@
"is_internal_customer", "is_internal_customer",
"is_discounted", "is_discounted",
"remarks", "remarks",
"repost_required",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -2125,15 +2124,6 @@
"label": "Write Off", "label": "Write Off",
"width": "50%" "width": "50%"
}, },
{
"default": "0",
"fieldname": "repost_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Repost Required",
"no_copy": 1,
"read_only": 1
},
{ {
"fieldname": "incoterm", "fieldname": "incoterm",
"fieldtype": "Link", "fieldtype": "Link",
@@ -2188,7 +2178,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2024-06-07 16:49:32.458402", "modified": "2024-07-18 15:30:39.428519",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -161,7 +161,6 @@ class SalesInvoice(SellingController):
project: DF.Link | None project: DF.Link | None
redeem_loyalty_points: DF.Check redeem_loyalty_points: DF.Check
remarks: DF.SmallText | None remarks: DF.SmallText | None
repost_required: DF.Check
represents_company: DF.Link | None represents_company: DF.Link | None
return_against: DF.Link | None return_against: DF.Link | None
rounded_total: DF.Currency rounded_total: DF.Currency
@@ -556,7 +555,6 @@ class SalesInvoice(SellingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
self.db_set("repost_required", 0)
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction": if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
update_company_current_month_sales(self.company) update_company_current_month_sales(self.company)
@@ -706,25 +704,23 @@ class SalesInvoice(SellingController):
data.sales_invoice = sales_invoice data.sales_invoice = sales_invoice
def on_update_after_submit(self): def on_update_after_submit(self):
if hasattr(self, "repost_required"): fields_to_check = [
fields_to_check = [ "additional_discount_account",
"additional_discount_account", "cash_bank_account",
"cash_bank_account", "account_for_change_amount",
"account_for_change_amount", "write_off_account",
"write_off_account", "loyalty_redemption_account",
"loyalty_redemption_account", "unrealized_profit_loss_account",
"unrealized_profit_loss_account", "is_opening",
"is_opening", ]
] child_tables = {
child_tables = { "items": ("income_account", "expense_account", "discount_account"),
"items": ("income_account", "expense_account", "discount_account"), "taxes": ("account_head",),
"taxes": ("account_head",), }
} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) if self.needs_repost:
if self.needs_repost: self.validate_for_repost()
self.validate_for_repost() self.repost_accounting_entries()
self.db_set("repost_required", self.needs_repost)
self.repost_accounting_entries()
def set_paid_amount(self): def set_paid_amount(self):
paid_amount = 0.0 paid_amount = 0.0

View File

@@ -2952,9 +2952,6 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) 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): 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. 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.

View File

@@ -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( vouchers, voucher_wise_amount = get_invoice_vouchers(
parties, tax_details, inv.company, party_type=party_type 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( advance_vouchers = get_advance_vouchers(
parties, parties,
company=inv.company, 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, to_date=tax_details.to_date,
party_type=party_type, party_type=party_type,
) )
taxable_vouchers = vouchers + advance_vouchers
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
tax_deducted_on_advances = 0 tax_deducted_on_advances = 0
if inv.doctype == "Purchase Invoice": 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 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"): def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
""" """
Use Payment Ledger to fetch unallocated Advance Payments Use Payment Ledger to fetch unallocated Advance Payments

View File

@@ -139,6 +139,7 @@ class ReceivablePayableReport:
paid_in_account_currency=0.0, paid_in_account_currency=0.0,
credit_note_in_account_currency=0.0, credit_note_in_account_currency=0.0,
outstanding_in_account_currency=0.0, outstanding_in_account_currency=0.0,
cost_center=ple.cost_center,
) )
self.get_invoices(ple) self.get_invoices(ple)
@@ -253,7 +254,7 @@ class ReceivablePayableReport:
row.paid -= amount row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency 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) row.cost_center = str(ple.cost_center)
def update_sub_total_row(self, row, party): def update_sub_total_row(self, row, party):

View File

@@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
si = si.submit() si = si.submit()
return si 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 = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
pe.paid_from = self.debit_to pe.paid_from = self.debit_to
pe.insert() pe.insert()
pe.submit() if not do_not_submit:
pe.submit()
return pe
def create_credit_note(self, docname, do_not_submit=False): def create_credit_note(self, docname, do_not_submit=False):
credit_note = create_sales_invoice( credit_note = create_sales_invoice(
@@ -984,3 +986,40 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
expected_data_after_payment, expected_data_after_payment,
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding], [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])

View File

@@ -315,8 +315,9 @@ def apply_conditions(query, pi, pii, filters):
def get_items(filters, additional_table_columns): def get_items(filters, additional_table_columns):
pi = frappe.qb.DocType("Purchase Invoice") doctype = "Purchase Invoice"
pii = frappe.qb.DocType("Purchase Invoice Item") pi = frappe.qb.DocType(doctype)
pii = frappe.qb.DocType(f"{doctype} Item")
Item = frappe.qb.DocType("Item") Item = frappe.qb.DocType("Item")
query = ( query = (
frappe.qb.from_(pi) frappe.qb.from_(pi)
@@ -353,6 +354,7 @@ def get_items(filters, additional_table_columns):
pi.mode_of_payment, pi.mode_of_payment,
) )
.where(pi.docstatus == 1) .where(pi.docstatus == 1)
.where(pii.parenttype == doctype)
) )
if filters.get("supplier"): if filters.get("supplier"):

View File

@@ -410,8 +410,9 @@ def apply_group_by_conditions(query, si, ii, filters):
def get_items(filters, additional_query_columns, additional_conditions=None): def get_items(filters, additional_query_columns, additional_conditions=None):
si = frappe.qb.DocType("Sales Invoice") doctype = "Sales Invoice"
sii = frappe.qb.DocType("Sales Invoice Item") si = frappe.qb.DocType(doctype)
sii = frappe.qb.DocType(f"{doctype} Item")
item = frappe.qb.DocType("Item") item = frappe.qb.DocType("Item")
query = ( query = (
@@ -459,6 +460,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
sii.qty, sii.qty,
) )
.where(si.docstatus == 1) .where(si.docstatus == 1)
.where(sii.parenttype == doctype)
) )
if additional_query_columns: if additional_query_columns:

View File

@@ -1598,6 +1598,18 @@ def auto_create_exchange_rate_revaluation_weekly() -> None:
create_err_and_its_journals(companies) 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): def get_payment_ledger_entries(gl_entries, cancel=0):
ple_map = [] ple_map = []
if gl_entries: if gl_entries:

View File

@@ -740,7 +740,7 @@ class TestDepreciationMethods(AssetSetup):
available_for_use_date="2030-06-06", available_for_use_date="2030-06-06",
is_existing_asset=1, is_existing_asset=1,
opening_number_of_booked_depreciations=2, opening_number_of_booked_depreciations=2,
opening_accumulated_depreciation=47095.89, opening_accumulated_depreciation=47178.08,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2032-12-31", depreciation_start_date="2032-12-31",
total_number_of_depreciations=3, total_number_of_depreciations=3,
@@ -748,7 +748,7 @@ class TestDepreciationMethods(AssetSetup):
) )
self.assertEqual(asset.status, "Draft") 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 = [ schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft") for d in get_depr_schedule(asset.name, "Draft")

View File

@@ -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 # 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 # 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 = 01/01/2022
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False) if row.depreciation_method in ("Straight Line", "Manual"):
days = date_diff(row.depreciation_start_date, from_date) + 1 prev_depreciation_start_date = add_months(
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) 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: if days <= 0:
frappe.throw( frappe.throw(
_( _(
@@ -682,20 +691,15 @@ def get_straight_line_or_manual_depr_amount(
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
else: else:
if row.daily_prorata_based: if row.daily_prorata_based:
amount = ( amount = flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
)
return get_daily_prorata_based_straight_line_depr( return get_daily_prorata_based_straight_line_depr(
asset, row, schedule_idx, number_of_pending_depreciations, amount asset, row, schedule_idx, number_of_pending_depreciations, amount
) )
else: else:
return ( depreciation_amount = (
flt(asset.gross_purchase_amount) flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- flt(asset.opening_accumulated_depreciation) ) / flt(row.total_number_of_depreciations)
- flt(row.expected_value_after_useful_life) return depreciation_amount
) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
def get_daily_prorata_based_straight_line_depr( def get_daily_prorata_based_straight_line_depr(
@@ -725,7 +729,11 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
) )
), ),
add_days( add_days(
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), add_months(
row.depreciation_start_date,
(row.frequency_of_depreciation * (asset.opening_number_of_booked_depreciations + 1))
* -1,
),
1, 1,
), ),
) )
@@ -904,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): def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
""" " """
Returns monthly depreciation amount when year changes Returns monthly depreciation amount when year changes
1. Calculate per day depr based on new year 1. Calculate per day depr based on new year
2. Calculate monthly amount based on new per day amount 2. Calculate monthly amount based on new per day amount

View File

@@ -75,6 +75,116 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
] ]
self.assertEqual(schedules, expected_schedules) 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 # Enable Checkbox to Calculate depreciation using total days in depreciation period
def test_daily_prorata_based_depr_after_enabling_configuration(self): def test_daily_prorata_based_depr_after_enabling_configuration(self):
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1) frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 1)

View File

@@ -39,16 +39,14 @@ def validate_filters(filters):
def get_data(filters): def get_data(filters):
po = frappe.qb.DocType("Purchase Order") po = frappe.qb.DocType("Purchase Order")
po_item = frappe.qb.DocType("Purchase Order Item") po_item = frappe.qb.DocType("Purchase Order Item")
pi = frappe.qb.DocType("Purchase Invoice")
pi_item = frappe.qb.DocType("Purchase Invoice Item") pi_item = frappe.qb.DocType("Purchase Invoice Item")
query = ( query = (
frappe.qb.from_(po) frappe.qb.from_(po)
.from_(po_item) .inner_join(po_item)
.on(po_item.parent == po.name)
.left_join(pi_item) .left_join(pi_item)
.on(pi_item.po_detail == po_item.name & pi_item.docstatus == 1) .on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
.left_join(pi)
.on(pi.name == pi_item.parent & pi.docstatus == 1)
.select( .select(
po.transaction_date.as_("date"), po.transaction_date.as_("date"),
po_item.schedule_date.as_("required_date"), po_item.schedule_date.as_("required_date"),

View File

@@ -2489,16 +2489,12 @@ class AccountsController(TransactionBase):
@frappe.whitelist() @frappe.whitelist()
def repost_accounting_entries(self): def repost_accounting_entries(self):
if self.repost_required: repost_ledger = frappe.new_doc("Repost Accounting Ledger")
repost_ledger = frappe.new_doc("Repost Accounting Ledger") repost_ledger.company = self.company
repost_ledger.company = self.company repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name}) repost_ledger.flags.ignore_permissions = True
repost_ledger.flags.ignore_permissions = True repost_ledger.insert()
repost_ledger.insert() repost_ledger.submit()
repost_ledger.submit()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
@frappe.whitelist() @frappe.whitelist()

View File

@@ -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): def update_terms(source_doc, target_doc, source_parent):
target_doc.payment_amount = -source_doc.payment_amount 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( doclist = get_mapped_doc(
doctype, doctype,
source_name, source_name,
@@ -654,6 +660,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
"doctype": doctype + " Item", "doctype": doctype + " Item",
"field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"}, "field_map": {"serial_no": "serial_no", "batch_no": "batch_no", "bom": "bom"},
"postprocess": update_item, "postprocess": update_item,
"condition": item_condition,
}, },
"Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms}, "Payment Schedule": {"doctype": "Payment Schedule", "postprocess": update_terms},
}, },

View File

@@ -456,6 +456,7 @@ scheduler_events = {
], ],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",
], ],
} }

View File

@@ -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") 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() @frappe.whitelist()
def add_item(**kwargs): def add_item(**kwargs):
if isinstance(kwargs, str): if isinstance(kwargs, str):
@@ -375,6 +381,11 @@ def add_item(**kwargs):
doc = frappe.get_doc("BOM Creator", kwargs.parent) doc = frappe.get_doc("BOM Creator", kwargs.parent)
item_info = get_item_details(kwargs.item_code) 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( kwargs.update(
{ {
"uom": item_info.stock_uom, "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.append("items", kwargs)
doc.save() doc.save()

View File

@@ -1781,6 +1781,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
apply_price_list(item, reset_plc_conversion) { apply_price_list(item, reset_plc_conversion) {
// We need to reset plc_conversion_rate sometimes because the call to // 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 // `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) { if (!reset_plc_conversion) {
this.frm.set_value("plc_conversion_rate", ""); this.frm.set_value("plc_conversion_rate", "");
} }

View File

@@ -754,7 +754,7 @@
"fieldname": "auto_err_frequency", "fieldname": "auto_err_frequency",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Frequency", "label": "Frequency",
"options": "Daily\nWeekly" "options": "Daily\nWeekly\nMonthly"
}, },
{ {
"default": "0", "default": "0",
@@ -808,7 +808,7 @@
"image_field": "company_logo", "image_field": "company_logo",
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2024-06-21 17:46:25.567565", "modified": "2024-07-24 18:17:56.413971",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",

View File

@@ -31,7 +31,7 @@ class Company(NestedSet):
accumulated_depreciation_account: DF.Link | None accumulated_depreciation_account: DF.Link | None
allow_account_creation_against_child_company: DF.Check allow_account_creation_against_child_company: DF.Check
asset_received_but_not_billed: DF.Link | None 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 auto_exchange_rate_revaluation: DF.Check
book_advance_payments_in_separate_party_account: DF.Check book_advance_payments_in_separate_party_account: DF.Check
capital_work_in_progress_account: DF.Link | None capital_work_in_progress_account: DF.Link | None

View File

@@ -163,7 +163,7 @@ def make_taxes_and_charges_template(company_name, doctype, template):
doc.flags.ignore_links = True doc.flags.ignore_links = True
doc.flags.ignore_validate = True doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
return doc return doc
@@ -196,7 +196,7 @@ def make_item_tax_template(company_name, template):
# Ingone validations to make doctypes faster # Ingone validations to make doctypes faster
doc.flags.ignore_links = True doc.flags.ignore_links = True
doc.flags.ignore_validate = True doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
return doc return doc
@@ -233,7 +233,7 @@ def get_or_create_account(company_name, account):
doc = frappe.get_doc(account) doc = frappe.get_doc(account)
doc.flags.ignore_links = True doc.flags.ignore_links = True
doc.flags.ignore_validate = 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 return doc

View File

@@ -6,7 +6,7 @@ import json
import frappe import frappe
from frappe.tests.utils import FrappeTestCase 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.doctype.account.test_account import get_inventory_account
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
@@ -2005,6 +2005,40 @@ class TestDeliveryNote(FrappeTestCase):
self.assertRaises(frappe.ValidationError, dn5.submit) 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): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@@ -1886,9 +1886,19 @@ class TestPurchaseReceipt(FrappeTestCase):
rate=100, rate=100,
rejected_qty=2, rejected_qty=2,
rejected_warehouse=rejected_warehouse, 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) 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].warehouse, rejected_warehouse)
self.assertEqual(pr_return.items[0].qty, 2.0 * -1) self.assertEqual(pr_return.items[0].qty, 2.0 * -1)
self.assertEqual(pr_return.items[0].rejected_qty, 0.0) self.assertEqual(pr_return.items[0].rejected_qty, 0.0)

View File

@@ -92,8 +92,10 @@ class SerialandBatchBundle(Document):
if self.type_of_transaction == "Maintenance": if self.type_of_transaction == "Maintenance":
return return
self.validate_serial_nos_duplicate() if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
self.check_future_entries_exists() self.validate_serial_nos_duplicate()
self.check_future_entries_exists()
self.set_is_outward() self.set_is_outward()
self.calculate_total_qty() self.calculate_total_qty()
self.set_warehouse() self.set_warehouse()
@@ -340,6 +342,9 @@ class SerialandBatchBundle(Document):
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
for d in self.entries: 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) d.incoming_rate = flt(rate, precision)
if d.qty: if d.qty:
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -393,32 +398,6 @@ class SerialandBatchBundle(Document):
self.calculate_qty_and_amount(save=True) self.calculate_qty_and_amount(save=True)
self.validate_quantity(row, qty_field=qty_field) 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): def validate_voucher_no(self):
if not (self.voucher_type and self.voucher_no): if not (self.voucher_type and self.voucher_no):
@@ -867,6 +846,9 @@ class SerialandBatchBundle(Document):
self.validate_serial_nos_inventory() self.validate_serial_nos_inventory()
def set_purchase_document_no(self): def set_purchase_document_no(self):
if self.flags.ignore_validate_serial_batch:
return
if not self.has_serial_no: if not self.has_serial_no:
return return

View File

@@ -83,7 +83,8 @@
"job_card", "job_card",
"amended_from", "amended_from",
"credit_note", "credit_note",
"is_return" "is_return",
"tab_connections"
], ],
"fields": [ "fields": [
{ {
@@ -683,6 +684,12 @@
"label": "Asset Repair", "label": "Asset Repair",
"options": "Asset Repair", "options": "Asset Repair",
"read_only": 1 "read_only": 1
},
{
"fieldname": "tab_connections",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",

View 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",
],
},
],
}

View File

@@ -434,7 +434,6 @@
}, },
{ {
"default": "1", "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. ", "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", "fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle",
"fieldtype": "Check", "fieldtype": "Check",
@@ -460,7 +459,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-07-15 17:18:23.872161", "modified": "2024-07-29 14:55:19.093508",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -3,6 +3,14 @@
frappe.query_reports["Product Bundle Balance"] = { frappe.query_reports["Product Bundle Balance"] = {
filters: [ filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{ {
fieldname: "date", fieldname: "date",
label: __("Date"), label: __("Date"),

View File

@@ -224,6 +224,9 @@ def get_stock_ledger_entries(filters, items):
.where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(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"): if date := filters.get("date"):
query = query.where(sle.posting_date <= date) query = query.where(sle.posting_date <= date)
else: else:
@@ -237,7 +240,7 @@ def get_stock_ledger_entries(filters, items):
if warehouse_details: if warehouse_details:
wh = frappe.qb.DocType("Warehouse") wh = frappe.qb.DocType("Warehouse")
query = query.where( query = query.where(
ExistsCriterion( sle.warehouse.isin(
frappe.qb.from_(wh) frappe.qb.from_(wh)
.select(wh.name) .select(wh.name)
.where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt)) .where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt))

View File

@@ -4,7 +4,7 @@ import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp 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 pypika import Order
from erpnext.stock.deprecated_serial_batch import ( 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", "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
"company": self.company, "company": self.company,
"is_rejected": self.is_rejected_entry(), "is_rejected": self.is_rejected_entry(),
"make_bundle_from_sle": 1,
} }
).make_serial_and_batch_bundle() ).make_serial_and_batch_bundle()
@@ -160,12 +161,13 @@ class SerialBatchBundle:
if msg: if msg:
error_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 + msg
) )
frappe.throw(_(error_msg)) frappe.throw(_(error_msg))
def set_serial_and_batch_bundle(self, sn_doc): 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}) self.sle.db_set({"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1})
if sn_doc.is_rejected: if sn_doc.is_rejected:
@@ -324,6 +326,9 @@ class SerialBatchBundle:
def set_warehouse_and_status_in_serial_nos(self): 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 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) serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
if not self.sle.serial_and_batch_bundle and self.sle.serial_no: if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
serial_nos = get_parsed_serial_nos(self.sle.serial_no) serial_nos = get_parsed_serial_nos(self.sle.serial_no)
@@ -338,7 +343,8 @@ class SerialBatchBundle:
status = "Delivered" status = "Delivered"
sn_table = frappe.qb.DocType("Serial No") sn_table = frappe.qb.DocType("Serial No")
(
query = (
frappe.qb.update(sn_table) frappe.qb.update(sn_table)
.set(sn_table.warehouse, warehouse) .set(sn_table.warehouse, warehouse)
.set( .set(
@@ -351,7 +357,19 @@ class SerialBatchBundle:
) )
.set(sn_table.company, self.sle.company) .set(sn_table.company, self.sle.company)
.where(sn_table.name.isin(serial_nos)) .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): def set_batch_no_in_serial_nos(self):
entries = frappe.get_all( entries = frappe.get_all(
@@ -915,6 +933,10 @@ class SerialBatchCreation:
if doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2: if doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2:
doc.voucher_no = "" 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() doc.save()
self.validate_qty(doc) self.validate_qty(doc)
@@ -1107,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." msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
frappe.throw(_(msg)) 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))): for _i in range(abs(cint(self.actual_qty))):
serial_no = make_autoname(self.serial_no_series, "Serial No") serial_no = make_autoname(self.serial_no_series, "Serial No")
sr_nos.append(serial_no) sr_nos.append(serial_no)
@@ -1124,6 +1150,7 @@ class SerialBatchCreation:
self.item_name, self.item_name,
self.description, self.description,
"Active", "Active",
voucher_no,
self.batch_no, self.batch_no,
) )
) )
@@ -1142,6 +1169,7 @@ class SerialBatchCreation:
"item_name", "item_name",
"description", "description",
"status", "status",
"purchase_document_no",
"batch_no", "batch_no",
] ]

View File

@@ -275,7 +275,9 @@ def repost_future_sle(
) )
affected_transactions.update(obj.affected_transactions) 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: if obj.new_items_found:
for _item_wh, data in distinct_item_warehouses.items(): for _item_wh, data in distinct_item_warehouses.items():
@@ -1588,9 +1590,11 @@ def get_stock_ledger_entries(
if not previous_sle.get("posting_date"): if not previous_sle.get("posting_date"):
previous_sle["posting_datetime"] = "1900-01-01 00:00:00" previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
else: else:
previous_sle["posting_datetime"] = get_combine_datetime( posting_time = previous_sle.get("posting_time")
previous_sle["posting_date"], previous_sle["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"): if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s" conditions += " and name!=%(name)s"

View File

@@ -23,18 +23,6 @@
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "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", "section_break_24",
"column_break_25", "column_break_25",
"set_warehouse", "set_warehouse",
@@ -48,10 +36,23 @@
"raw_materials_supplied_section", "raw_materials_supplied_section",
"set_reserve_warehouse", "set_reserve_warehouse",
"supplied_items", "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", "distribute_additional_costs_based_on",
"additional_costs", "additional_costs",
"total_additional_costs", "total_additional_costs",
"tab_other_info",
"order_status_section", "order_status_section",
"status", "status",
"column_break_39", "column_break_39",
@@ -59,7 +60,8 @@
"printing_settings_section", "printing_settings_section",
"select_print_heading", "select_print_heading",
"column_break_43", "column_break_43",
"letter_head" "letter_head",
"tab_connections"
], ],
"fields": [ "fields": [
{ {
@@ -95,7 +97,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_global_search": 1, "in_global_search": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Supplier", "label": "Job Worker",
"options": "Supplier", "options": "Supplier",
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
@@ -107,7 +109,7 @@
"fieldname": "supplier_name", "fieldname": "supplier_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
"label": "Supplier Name", "label": "Job Worker Name",
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1
}, },
@@ -115,7 +117,7 @@
"depends_on": "supplier", "depends_on": "supplier",
"fieldname": "supplier_warehouse", "fieldname": "supplier_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Warehouse", "label": "Job Worker Warehouse",
"options": "Warehouse", "options": "Warehouse",
"reqd": 1 "reqd": 1
}, },
@@ -166,9 +168,8 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1, "fieldname": "tab_address_and_contact",
"fieldname": "address_and_contact_section", "fieldtype": "Tab Break",
"fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
}, },
{ {
@@ -176,14 +177,14 @@
"fetch_if_empty": 1, "fetch_if_empty": 1,
"fieldname": "supplier_address", "fieldname": "supplier_address",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Address", "label": "Job Worker Address",
"options": "Address", "options": "Address",
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Supplier Address Details", "label": "Job Worker Address Details",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -191,7 +192,7 @@
"fetch_if_empty": 1, "fetch_if_empty": 1,
"fieldname": "contact_person", "fieldname": "contact_person",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Contact", "label": "Job Worker Contact",
"options": "Contact", "options": "Contact",
"print_hide": 1 "print_hide": 1
}, },
@@ -337,11 +338,9 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"collapsible_depends_on": "total_additional_costs",
"depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)",
"fieldname": "additional_costs_section", "fieldname": "tab_additional_costs",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Additional Costs" "label": "Additional Costs"
}, },
{ {
@@ -449,6 +448,17 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "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", "icon": "fa fa-file-text",

View File

@@ -23,18 +23,6 @@
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "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", "sec_warehouse",
"set_warehouse", "set_warehouse",
"rejected_warehouse", "rejected_warehouse",
@@ -53,23 +41,36 @@
"get_current_stock", "get_current_stock",
"raw_material_details", "raw_material_details",
"supplied_items", "supplied_items",
"additional_costs_section",
"distribute_additional_costs_based_on",
"additional_costs",
"total_additional_costs",
"section_break_46", "section_break_46",
"in_words", "in_words",
"bill_no", "bill_no",
"bill_date", "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", "more_info",
"status",
"column_break_39",
"per_returned",
"section_break_47",
"amended_from", "amended_from",
"range", "range",
"column_break4", "column_break4",
"represents_company", "represents_company",
"order_status_section",
"status",
"column_break_39",
"per_returned",
"subscription_detail", "subscription_detail",
"auto_repeat", "auto_repeat",
"printing_settings", "printing_settings",
@@ -84,7 +85,8 @@
"transporter_name", "transporter_name",
"column_break5", "column_break5",
"lr_no", "lr_no",
"lr_date" "lr_date",
"tab_connections"
], ],
"fields": [ "fields": [
{ {
@@ -112,7 +114,7 @@
"fieldname": "supplier", "fieldname": "supplier",
"fieldtype": "Link", "fieldtype": "Link",
"in_global_search": 1, "in_global_search": 1,
"label": "Supplier", "label": "Job Worker",
"options": "Supplier", "options": "Supplier",
"print_hide": 1, "print_hide": 1,
"print_width": "150px", "print_width": "150px",
@@ -127,7 +129,7 @@
"fieldname": "supplier_name", "fieldname": "supplier_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
"label": "Supplier Name", "label": "Job Worker Name",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -174,15 +176,14 @@
"width": "150px" "width": "150px"
}, },
{ {
"collapsible": 1, "fieldname": "tab_addresses",
"fieldname": "section_addresses", "fieldtype": "Tab Break",
"fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
}, },
{ {
"fieldname": "supplier_address", "fieldname": "supplier_address",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Select Supplier Address", "label": "Select Job Worker Address",
"options": "Address", "options": "Address",
"print_hide": 1 "print_hide": 1
}, },
@@ -269,7 +270,7 @@
{ {
"fieldname": "supplier_warehouse", "fieldname": "supplier_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Warehouse", "label": "Job Worker Warehouse",
"no_copy": 1, "no_copy": 1,
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1, "print_hide": 1,
@@ -414,6 +415,7 @@
"width": "50%" "width": "50%"
}, },
{ {
"collapsible": 1,
"fieldname": "subscription_detail", "fieldname": "subscription_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Auto Repeat Detail" "label": "Auto Repeat Detail"
@@ -571,10 +573,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "section_break_47",
"fieldtype": "Section Break"
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
@@ -598,11 +596,9 @@
"options": "Project" "options": "Project"
}, },
{ {
"collapsible": 1,
"collapsible_depends_on": "total_additional_costs",
"depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)",
"fieldname": "additional_costs_section", "fieldname": "tab_additional_costs",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Additional Costs" "label": "Additional Costs"
}, },
{ {
@@ -643,7 +639,7 @@
{ {
"fieldname": "supplier_delivery_note", "fieldname": "supplier_delivery_note",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Supplier Delivery Note" "label": "Job Worker Delivery Note"
}, },
{ {
"fieldname": "raw_materials_consumed_section", "fieldname": "raw_materials_consumed_section",
@@ -658,6 +654,23 @@
{ {
"fieldname": "column_break_uinr", "fieldname": "column_break_uinr",
"fieldtype": "Column Break" "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, "in_create": 1,