mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-24 03:19:49 +00:00
Compare commits
130 Commits
pot_develo
...
michelleal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce21653266 | ||
|
|
755c162093 | ||
|
|
e177ece008 | ||
|
|
47c2127a31 | ||
|
|
efadf94cf3 | ||
|
|
b1c23ba85c | ||
|
|
82b02d6c9b | ||
|
|
8040544216 | ||
|
|
34ad9d33ff | ||
|
|
113362e2d9 | ||
|
|
165ec2ed53 | ||
|
|
faff84c6e5 | ||
|
|
4a4e1d36f2 | ||
|
|
c930f8ba9d | ||
|
|
c8af544ef3 | ||
|
|
0f0b19688a | ||
|
|
4253caf910 | ||
|
|
213b2ba942 | ||
|
|
e78e948b7c | ||
|
|
56620785a0 | ||
|
|
751a25c4b7 | ||
|
|
05b92c5321 | ||
|
|
9ade269b7a | ||
|
|
991069bfbc | ||
|
|
3617b41b95 | ||
|
|
03f3ab522f | ||
|
|
3ffac73598 | ||
|
|
6d2a36c4f9 | ||
|
|
5040e3ca7d | ||
|
|
da22a8df42 | ||
|
|
511a0b9f37 | ||
|
|
5d58eb67a6 | ||
|
|
bb8c9b5a58 | ||
|
|
59d5beee20 | ||
|
|
bb877f4a6b | ||
|
|
cfe2ae604b | ||
|
|
a4311e345d | ||
|
|
61576ca030 | ||
|
|
281198456d | ||
|
|
ac629ede79 | ||
|
|
fb2aa7d205 | ||
|
|
9fe47ac101 | ||
|
|
d811fdf675 | ||
|
|
dd5a5e4919 | ||
|
|
f620ef20ae | ||
|
|
40c166a0a0 | ||
|
|
33a0a529a5 | ||
|
|
8624aeca54 | ||
|
|
a694390a12 | ||
|
|
4986f28a89 | ||
|
|
0e5e503b42 | ||
|
|
fd71d8af52 | ||
|
|
1c7f7c8d1a | ||
|
|
93ee922c5f | ||
|
|
23fed831a0 | ||
|
|
0ecfa709d8 | ||
|
|
c5d68333c9 | ||
|
|
ff3a5058c1 | ||
|
|
5ec9df8d1c | ||
|
|
0b6e7f83cd | ||
|
|
f0768010d9 | ||
|
|
cbb749a3a5 | ||
|
|
8eff168d76 | ||
|
|
25dac1f18e | ||
|
|
cb522f8f22 | ||
|
|
21d8b09f47 | ||
|
|
e9c1a7e3e3 | ||
|
|
24b26627e7 | ||
|
|
9aaf6e47d5 | ||
|
|
4931b74b3f | ||
|
|
532bc163f6 | ||
|
|
4c3ec767ce | ||
|
|
0d77e0b0cd | ||
|
|
723ac0ffc4 | ||
|
|
9a0894fd65 | ||
|
|
096ec2db6a | ||
|
|
3b481ae656 | ||
|
|
ad31b63537 | ||
|
|
3e19041fa3 | ||
|
|
ce81fd9ba6 | ||
|
|
07281d30bb | ||
|
|
764dd12b10 | ||
|
|
77590e6077 | ||
|
|
40b59de4cd | ||
|
|
302339998f | ||
|
|
0e817f42ef | ||
|
|
8c6d666163 | ||
|
|
4ab6134306 | ||
|
|
e062ec38b0 | ||
|
|
fc4e5f165c | ||
|
|
ce2b9e0f1a | ||
|
|
06e2d7265c | ||
|
|
9cd3374101 | ||
|
|
89c458e229 | ||
|
|
ffc5bbfa4c | ||
|
|
92a6fc8c2e | ||
|
|
8ccf8c0ea2 | ||
|
|
972329cc16 | ||
|
|
35981b8730 | ||
|
|
6e1fc33d01 | ||
|
|
c8e85f4bb7 | ||
|
|
bf2c9a7f41 | ||
|
|
be2648245b | ||
|
|
65f80abf2f | ||
|
|
9b463753b7 | ||
|
|
9e93bc5819 | ||
|
|
5d4578a172 | ||
|
|
45391c951b | ||
|
|
741fc54eca | ||
|
|
6d83c11d8e | ||
|
|
e71cb4eab7 | ||
|
|
07fc952a43 | ||
|
|
09f429ffba | ||
|
|
fe46e1d089 | ||
|
|
e81373bb6a | ||
|
|
a467888a67 | ||
|
|
06c5334f2a | ||
|
|
f3fda9ce98 | ||
|
|
cf55c2ab3d | ||
|
|
da4ed90a3e | ||
|
|
656e363aef | ||
|
|
372b5a4ca7 | ||
|
|
5e3359c5c5 | ||
|
|
62fc3bd586 | ||
|
|
e7ee508de4 | ||
|
|
4b415987f9 | ||
|
|
cfda332faf | ||
|
|
0ba4bff943 | ||
|
|
9d9331408f | ||
|
|
a2f878e1d1 |
2
.github/helper/update_pot_file.sh
vendored
2
.github/helper/update_pot_file.sh
vendored
@@ -37,4 +37,4 @@ gh auth setup-git
|
||||
git push -u upstream "${branch_name}"
|
||||
|
||||
echo "Creating a PR..."
|
||||
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/erpnext
|
||||
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" --reviewer ${PR_REVIEWER} -R frappe/erpnext
|
||||
|
||||
1
.github/workflows/generate-pot-file.yml
vendored
1
.github/workflows/generate-pot-file.yml
vendored
@@ -36,3 +36,4 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
BASE_BRANCH: ${{ matrix.branch }}
|
||||
PR_REVIEWER: barredterra # change to your GitHub username if you copied this file
|
||||
|
||||
@@ -30,7 +30,7 @@ ERPNext as a monolith includes the following areas for managing businesses:
|
||||
1. [Quality Management](https://erpnext.com/docs/user/manual/en/quality-management)
|
||||
1. [Manufacturing](https://erpnext.com/open-source-manufacturing-erp-software)
|
||||
1. [Website Management](https://erpnext.com/open-source-website-builder-software)
|
||||
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
|
||||
1. [Customize ERPNext](https://docs.erpnext.com/docs/v13/user/manual/en/customize-erpnext)
|
||||
1. [And More](https://erpnext.com/docs/user/manual/en/)
|
||||
|
||||
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
**/setup/setup_wizard/data/uom_data.json,erpnext.gettext.extractors.uom_data.extract
|
||||
**/setup/doctype/incoterm/incoterms.csv,erpnext.gettext.extractors.incoterms.extract
|
||||
|
||||
|
@@ -4,5 +4,7 @@ files:
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
|
||||
@@ -73,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": [
|
||||
{
|
||||
@@ -475,6 +477,18 @@
|
||||
"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",
|
||||
@@ -482,7 +496,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-12 00:24:20.957726",
|
||||
"modified": "2024-07-26 06:48:52.714630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -35,6 +35,7 @@ class AccountsSettings(Document):
|
||||
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": "2024-03-27 13:09:58.366953",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -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,14 +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.repost_accounting_entries()
|
||||
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
|
||||
|
||||
@@ -165,8 +165,25 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("purchase_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
|
||||
@@ -37,7 +37,6 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -1214,90 +1213,82 @@ class PaymentEntry(AccountsController):
|
||||
self.make_advance_gl_entries(cancel=cancel)
|
||||
|
||||
def add_party_gl_entries(self, gl_entries):
|
||||
if self.party_account:
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
against_account = self.paid_from
|
||||
|
||||
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
|
||||
|
||||
party_gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
if (
|
||||
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and d.allocated_amount < 0
|
||||
and (
|
||||
(party_account_type == "Receivable" and self.payment_type == "Pay")
|
||||
or (party_account_type == "Payable" and self.payment_type == "Receive")
|
||||
)
|
||||
):
|
||||
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
|
||||
for d in self.get("references"):
|
||||
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
if is_return and not reverse_dr_or_cr:
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: abs(allocated_amount_in_company_currency),
|
||||
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
"cost_center": cost_center,
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
}
|
||||
)
|
||||
gl_entries.append(gle)
|
||||
gl_entries.append(gle)
|
||||
|
||||
def make_advance_gl_entries(
|
||||
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -491,10 +491,15 @@ def make_payment_request(**args):
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
"make_sales_invoice": args.order_type == "Shopping Cart",
|
||||
"mute_email": args.mute_email
|
||||
or args.order_type == "Shopping Cart"
|
||||
or gateway_account.get("payment_channel", "Email") != "Email",
|
||||
"make_sales_invoice": (
|
||||
args.make_sales_invoice # new standard
|
||||
or args.order_type == "Shopping Cart" # compat for webshop app
|
||||
),
|
||||
"mute_email": (
|
||||
args.mute_email # new standard
|
||||
or args.order_type == "Shopping Cart" # compat for webshop app
|
||||
or gateway_account.get("payment_channel", "Email") != "Email"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -509,7 +514,8 @@ def make_payment_request(**args):
|
||||
for dimension in get_accounting_dimensions():
|
||||
pr.update({dimension: ref_doc.get(dimension)})
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -194,7 +194,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
order_type="Shopping Cart",
|
||||
make_sales_invoice=True,
|
||||
mute_email=True,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,9 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -1311,6 +1313,69 @@ class TestPricingRule(unittest.TestCase):
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
def test_ignore_pricing_rule_for_credit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
selling=1,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
si.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 80)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
credit_note = make_return_doc(si.doctype, si.name)
|
||||
credit_note.save()
|
||||
self.assertEqual(credit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(credit_note.pricing_rules, [])
|
||||
self.assertEqual(credit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(credit_note.items[0].rate, 80)
|
||||
self.assertEqual(credit_note.items[0].pricing_rules, None)
|
||||
|
||||
credit_note.delete()
|
||||
si.cancel()
|
||||
|
||||
def test_ignore_pricing_rule_for_debit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
|
||||
item = pi.items[0]
|
||||
pi.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 40)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
# create debit note from purchase invoice
|
||||
debit_note = make_return_doc(pi.doctype, pi.name)
|
||||
debit_note.save()
|
||||
|
||||
self.assertEqual(debit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(debit_note.pricing_rules, [])
|
||||
self.assertEqual(debit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(debit_note.items[0].rate, 40)
|
||||
self.assertEqual(debit_note.items[0].pricing_rules, None)
|
||||
|
||||
debit_note.delete()
|
||||
pi.cancel()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Promotional Scheme", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("for_price_list", "price_discount_slabs", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
selling: doc.selling,
|
||||
buying: doc.buying,
|
||||
currency: doc.currency,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.trigger("toggle_reqd_apply_on");
|
||||
|
||||
@@ -51,6 +51,7 @@ price_discount_fields = [
|
||||
"discount_percentage",
|
||||
"validate_applied_rule",
|
||||
"apply_multiple_pricing_rules",
|
||||
"for_price_list",
|
||||
]
|
||||
|
||||
product_discount_fields = [
|
||||
@@ -63,6 +64,7 @@ product_discount_fields = [
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"apply_multiple_pricing_rules",
|
||||
"round_free_qty",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"rate",
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
"for_price_list",
|
||||
"section_break_11",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -120,6 +121,13 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Discount Percentage"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.rate_or_discount!=\"Rate\"",
|
||||
"fieldname": "for_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -169,7 +177,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:22.448265",
|
||||
"modified": "2024-07-23 12:33:46.574950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Price Discount",
|
||||
|
||||
@@ -19,6 +19,7 @@ class PromotionalSchemePriceDiscount(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
for_price_list: DF.Link | None
|
||||
max_amount: DF.Currency
|
||||
max_qty: DF.Float
|
||||
min_amount: DF.Currency
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"column_break_9",
|
||||
"free_item_uom",
|
||||
"free_item_rate",
|
||||
"round_free_qty",
|
||||
"section_break_12",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@@ -181,12 +182,18 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "round_free_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Round Free Qty"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:22.605892",
|
||||
"modified": "2024-07-22 17:25:07.880984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Product Discount",
|
||||
@@ -195,4 +202,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
"20",
|
||||
]
|
||||
recurse_for: DF.Float
|
||||
round_free_qty: DF.Check
|
||||
rule_description: DF.SmallText
|
||||
same_item: DF.Check
|
||||
threshold_percentage: DF.Percent
|
||||
|
||||
@@ -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",
|
||||
@@ -1604,15 +1604,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",
|
||||
@@ -1640,7 +1631,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
|
||||
@@ -797,19 +796,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)
|
||||
self.repost_accounting_entries()
|
||||
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"
|
||||
@@ -1710,6 +1707,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
|
||||
|
||||
@@ -1751,8 +1751,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",
|
||||
@@ -1767,7 +1765,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)
|
||||
|
||||
@@ -2024,8 +2024,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
["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):
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"landed_cost_voucher_amount",
|
||||
"rm_supp_cost",
|
||||
@@ -958,12 +959,22 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
|
||||
"fieldname": "sales_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Sales Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-14 11:57:07.171700",
|
||||
"modified": "2024-07-19 12:12:42.449298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_incoming_rate: DF.Currency
|
||||
sales_invoice_item: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -161,7 +136,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(
|
||||
@@ -596,49 +571,6 @@ cur_frm.cscript["Make Delivery Note"] = function () {
|
||||
});
|
||||
};
|
||||
|
||||
cur_frm.fields_dict.cash_bank_account.get_query = function (doc) {
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Cash", "Bank"]],
|
||||
["Account", "root_type", "=", "Asset"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.fields_dict.write_off_account.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Write off cost center
|
||||
//-----------------------
|
||||
cur_frm.fields_dict.write_off_cost_center.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Cost Center in Details Table
|
||||
// -----------------------------
|
||||
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.cscript.income_account = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "income_account");
|
||||
};
|
||||
@@ -651,28 +583,6 @@ cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
|
||||
};
|
||||
|
||||
cur_frm.set_query("debit_to", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
account_type: "Receivable",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
cur_frm.set_query("asset", "items", function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Asset", "item_code", "=", d.item_code],
|
||||
["Asset", "docstatus", "=", 1],
|
||||
["Asset", "status", "in", ["Submitted", "Partially Depreciated", "Fully Depreciated"]],
|
||||
["Asset", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("customer", "tax_id", "tax_id");
|
||||
@@ -682,71 +592,132 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
frm.set_df_property("packed_items", "cannot_add_rows", true);
|
||||
frm.set_df_property("packed_items", "cannot_delete_rows", true);
|
||||
|
||||
frm.set_query("account_for_change_amount", function () {
|
||||
frm.set_query("cash_bank_account", function (doc) {
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Cash", "Bank"]],
|
||||
["Account", "root_type", "=", "Asset"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", ["Cash", "Bank"]],
|
||||
company: frm.doc.company,
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", "items", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("unrealized_profit_loss_account", function () {
|
||||
frm.set_query("debit_to", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
account_type: "Receivable",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("asset", "items", function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Asset", "item_code", "=", row.item_code],
|
||||
["Asset", "docstatus", "=", 1],
|
||||
["Asset", "status", "in", ["Submitted", "Partially Depreciated", "Fully Depreciated"]],
|
||||
["Asset", "company", "=", doc.company],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account_for_change_amount", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
account_type: ["in", ["Cash", "Bank"]],
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("unrealized_profit_loss_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
root_type: "Liability",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("adjustment_against", function () {
|
||||
frm.set_query("adjustment_against", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
customer: frm.doc.customer,
|
||||
company: doc.company,
|
||||
customer: doc.customer,
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("additional_discount_account", function () {
|
||||
frm.set_query("additional_discount_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
report_type: "Profit and Loss",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("income_account", "items", function () {
|
||||
frm.set_query("income_account", "items", function (doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_income_account",
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
company: doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
(frm.custom_make_buttons = {
|
||||
frm.custom_make_buttons = {
|
||||
"Delivery Note": "Delivery",
|
||||
"Sales Invoice": "Return / Credit Note",
|
||||
"Payment Request": "Payment Request",
|
||||
"Payment Entry": "Payment",
|
||||
}),
|
||||
(frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
query: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet",
|
||||
filters: { project: doc.project },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// discount account
|
||||
frm.fields_dict["items"].grid.get_field("discount_account").get_query = function (doc) {
|
||||
frm.set_query("time_sheet", "timesheets", function (doc, cdt, cdn) {
|
||||
return {
|
||||
query: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet",
|
||||
filters: { project: doc.project },
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("discount_account", "items", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
report_type: "Profit and Loss",
|
||||
@@ -754,9 +725,9 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["items"].grid.get_field("deferred_revenue_account").get_query = function (doc) {
|
||||
frm.set_query("deferred_revenue_account", "items", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
root_type: "Liability",
|
||||
@@ -764,7 +735,7 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("company_address", function (doc) {
|
||||
if (!doc.company) {
|
||||
@@ -793,25 +764,23 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
};
|
||||
});
|
||||
|
||||
// set get_query for loyalty redemption account
|
||||
frm.fields_dict["loyalty_redemption_account"].get_query = function () {
|
||||
frm.set_query("loyalty_redemption_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
// set get_query for loyalty redemption cost center
|
||||
frm.fields_dict["loyalty_redemption_cost_center"].get_query = function () {
|
||||
frm.set_query("loyalty_redemption_cost_center", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
},
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
|
||||
@@ -215,7 +215,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -2128,15 +2127,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",
|
||||
@@ -2205,7 +2195,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-23 14:02:28.549041",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -166,7 +166,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
|
||||
@@ -569,7 +568,6 @@ class SalesInvoice(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("repost_required", 0)
|
||||
|
||||
if self.coupon_code:
|
||||
update_coupon_code_count(self.coupon_code, "cancelled")
|
||||
@@ -722,25 +720,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)
|
||||
self.repost_accounting_entries()
|
||||
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
|
||||
@@ -1326,6 +1322,10 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
if item.is_fixed_asset:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
@@ -1384,37 +1384,33 @@ class SalesInvoice(SellingController):
|
||||
self.set_asset_status(asset)
|
||||
|
||||
else:
|
||||
# Do not book income for transfer within same company
|
||||
if not self.is_internal_transfer():
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
income_account = (
|
||||
item.income_account
|
||||
if (not item.enable_deferred_revenue or self.is_return)
|
||||
else item.deferred_revenue_account
|
||||
)
|
||||
|
||||
amount, base_amount = self.get_amount_and_base_amount(
|
||||
item, enable_discount_accounting
|
||||
)
|
||||
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
account_currency = get_account_currency(income_account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": income_account,
|
||||
"against": self.customer,
|
||||
"credit": flt(base_amount, item.precision("base_net_amount")),
|
||||
"credit_in_account_currency": (
|
||||
flt(base_amount, item.precision("base_net_amount"))
|
||||
if account_currency == self.company_currency
|
||||
else flt(amount, item.precision("net_amount"))
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# expense account gl entries
|
||||
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
|
||||
@@ -2965,9 +2965,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.
|
||||
@@ -3099,6 +3096,84 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
cust_doc.save()
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a dimension and make it mandatory
|
||||
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
|
||||
dim = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
|
||||
}
|
||||
)
|
||||
dim.save()
|
||||
else:
|
||||
dim = frappe.get_doc(
|
||||
"Accounting Dimension",
|
||||
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
|
||||
)
|
||||
dim.disabled = False
|
||||
dim.dimension_defaults = []
|
||||
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
|
||||
dim.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
|
||||
)
|
||||
si.department = "All Departments"
|
||||
si.save().submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"department": "All Departments",
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
dim.disabled = True
|
||||
dim.save()
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
|
||||
@@ -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":
|
||||
@@ -369,12 +375,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
AND ja.party in %s
|
||||
AND j.apply_tds = 1
|
||||
AND j.tax_withholding_category = %s
|
||||
AND j.company = %s
|
||||
""",
|
||||
(
|
||||
tax_details.from_date,
|
||||
tax_details.to_date,
|
||||
tuple(parties),
|
||||
tax_details.get("tax_withholding_category"),
|
||||
company,
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -387,6 +395,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
|
||||
@@ -477,6 +499,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
"unallocated_amount": (">", 0),
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": inv.company,
|
||||
}
|
||||
|
||||
field = "sum(tax_withholding_net_total)"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
@@ -984,3 +986,40 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
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])
|
||||
|
||||
@@ -104,7 +104,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
if total_credit:
|
||||
data.append(total_credit)
|
||||
|
||||
report_summary = get_bs_summary(
|
||||
report_summary, primitive_summary = get_bs_summary(
|
||||
companies,
|
||||
asset,
|
||||
liability,
|
||||
@@ -175,7 +175,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
report_summary = get_pl_summary(
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
)
|
||||
|
||||
|
||||
@@ -209,6 +209,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_cr_dr_notes",
|
||||
label: __("Ignore System Generated Credit / Debit Notes"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -236,6 +236,20 @@ def get_conditions(filters):
|
||||
if err_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||
|
||||
if filters.get("ignore_cr_dr_notes"):
|
||||
system_generated_cr_dr_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"company": filters.get("company"),
|
||||
"docstatus": 1,
|
||||
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
|
||||
"is_system_generated": 1,
|
||||
},
|
||||
as_list=True,
|
||||
)
|
||||
if system_generated_cr_dr_journals:
|
||||
filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]})
|
||||
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
|
||||
@@ -2,13 +2,32 @@
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def test_foreign_account_balance_after_exchange_rate_revaluation(self):
|
||||
"""
|
||||
Checks the correctness of balance after exchange rate revaluation
|
||||
@@ -248,3 +267,68 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
)
|
||||
)
|
||||
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
|
||||
|
||||
def test_ignore_cr_dr_notes_filter(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
cr_note = make_return_doc(si.doctype, si.name)
|
||||
cr_note.submit()
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = si.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = si.customer
|
||||
pr.receivable_payable_account = si.debit_to
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name]
|
||||
payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
system_generated_journal = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"voucher_type": "Credit Note",
|
||||
"is_system_generated": True,
|
||||
},
|
||||
fields=["name"],
|
||||
)
|
||||
self.assertEqual(len(system_generated_journal), 1)
|
||||
expected = set([si.name, cr_note.name, system_generated_journal[0].name])
|
||||
# Without ignore_cr_dr_notes
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# Without ignore_cr_dr_notes
|
||||
expected = set([si.name, cr_note.name])
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": si.company,
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
actual = set([x.voucher_no for x in data if x.voucher_no])
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1614,6 +1614,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:
|
||||
|
||||
@@ -188,11 +188,21 @@ frappe.ui.form.on("Asset", {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
|
||||
if (frm.doc.is_composite_asset) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
|
||||
args: {
|
||||
asset: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -775,11 +785,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");
|
||||
}
|
||||
|
||||
@@ -221,7 +221,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_composite_asset",
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
@@ -580,7 +579,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-07 22:27:14.733839",
|
||||
"modified": "2024-08-01 16:39:09.340973",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -268,10 +268,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"),
|
||||
)
|
||||
@@ -1036,6 +1036,14 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
return asset.get_value_after_depreciation(finance_book)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_active_capitalization(asset):
|
||||
active_capitalizations = frappe.db.count(
|
||||
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
||||
)
|
||||
return active_capitalizations > 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,20 +691,15 @@ 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(
|
||||
@@ -725,7 +729,16 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
|
||||
)
|
||||
),
|
||||
add_days(
|
||||
get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)),
|
||||
get_last_day(
|
||||
add_months(
|
||||
row.depreciation_start_date,
|
||||
(
|
||||
row.frequency_of_depreciation
|
||||
* (asset.opening_number_of_booked_depreciations + 1)
|
||||
)
|
||||
* -1,
|
||||
),
|
||||
),
|
||||
1,
|
||||
),
|
||||
)
|
||||
@@ -904,7 +917,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,116 @@ 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)
|
||||
|
||||
@@ -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 & pi_item.docstatus == 1)
|
||||
.left_join(pi)
|
||||
.on(pi.name == pi_item.parent & pi.docstatus == 1)
|
||||
.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"),
|
||||
|
||||
@@ -14,6 +14,9 @@ from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_
|
||||
def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
"""updates last_purchase_rate in item table for each item"""
|
||||
|
||||
if doc.get("is_internal_supplier"):
|
||||
return
|
||||
|
||||
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
|
||||
|
||||
for d in doc.get("items"):
|
||||
|
||||
@@ -85,7 +85,6 @@ force_item_fields = (
|
||||
"brand",
|
||||
"stock_uom",
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"pricing_rules",
|
||||
"weight_per_unit",
|
||||
"weight_uom",
|
||||
@@ -743,7 +742,6 @@ class AccountsController(TransactionBase):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
if item.get(fieldname) is None or fieldname in force_item_fields:
|
||||
@@ -753,7 +751,10 @@ class AccountsController(TransactionBase):
|
||||
fieldname
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "item_tax_rate" and not (
|
||||
self.get("is_return") and self.get("return_against")
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
elif fieldname == "serial_no":
|
||||
# Ensure that serial numbers are matched against Stock UOM
|
||||
item_conversion_factor = item.get("conversion_factor") or 1.0
|
||||
@@ -2460,6 +2461,15 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
# update dimesions
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
@@ -2517,16 +2527,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()
|
||||
|
||||
@@ -314,18 +314,22 @@ class BuyingController(SubcontractingController):
|
||||
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||
)
|
||||
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ item.rm_supp_cost
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("rate_difference_with_purchase_invoice"))
|
||||
@@ -336,72 +340,88 @@ class BuyingController(SubcontractingController):
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||
"""
|
||||
Override item rate with incoming rate for internal stock transfer
|
||||
"""
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice"):
|
||||
return
|
||||
|
||||
if not (self.doctype == "Purchase Receipt" or self.get("update_stock")):
|
||||
return
|
||||
|
||||
if cint(self.get("is_return")):
|
||||
# Get outgoing rate based on original item cost based on valuation method
|
||||
return
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
if allow_at_arms_length_price:
|
||||
return
|
||||
|
||||
self.set_sales_incoming_rate_for_internal_transfer()
|
||||
|
||||
for d in self.get("items"):
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
|
||||
if d.rate == d.sales_incoming_rate:
|
||||
continue
|
||||
|
||||
d.rate = d.sales_incoming_rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
|
||||
def set_sales_incoming_rate_for_internal_transfer(self):
|
||||
"""
|
||||
Set incoming rate from the sales transaction against which the
|
||||
purchase is made (internal transfer)
|
||||
"""
|
||||
ref_doctype_map = {
|
||||
"Purchase Order": "Sales Order Item",
|
||||
"Purchase Receipt": "Delivery Note Item",
|
||||
"Purchase Invoice": "Sales Invoice Item",
|
||||
}
|
||||
|
||||
ref_doctype = ref_doctype_map.get(self.doctype)
|
||||
items = self.get("items")
|
||||
for d in items:
|
||||
if not cint(self.get("is_return")):
|
||||
# Get outgoing rate based on original item cost based on valuation method
|
||||
for d in self.get("items"):
|
||||
if not d.get(frappe.scrub(ref_doctype)):
|
||||
posting_time = self.get("posting_time")
|
||||
if not posting_time:
|
||||
posting_time = nowtime()
|
||||
|
||||
if not d.get(frappe.scrub(ref_doctype)):
|
||||
posting_time = self.get("posting_time")
|
||||
if not posting_time and self.doctype == "Purchase Order":
|
||||
posting_time = nowtime()
|
||||
outgoing_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.get("from_warehouse"),
|
||||
"posting_date": self.get("posting_date") or self.get("transaction_date"),
|
||||
"posting_time": posting_time,
|
||||
"qty": -1 * flt(d.get("stock_qty")),
|
||||
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"voucher_detail_no": d.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
outgoing_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.get("from_warehouse"),
|
||||
"posting_date": self.get("posting_date") or self.get("transaction_date"),
|
||||
"posting_time": posting_time,
|
||||
"qty": -1 * flt(d.get("stock_qty")),
|
||||
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"voucher_detail_no": d.name,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = (
|
||||
"incoming_rate"
|
||||
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
|
||||
else "rate"
|
||||
)
|
||||
rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
)
|
||||
|
||||
if self.is_internal_transfer():
|
||||
if self.doctype == "Purchase Receipt" or self.get("update_stock"):
|
||||
if rate != d.rate:
|
||||
d.rate = rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
|
||||
).format(d.idx),
|
||||
alert=1,
|
||||
)
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
d.sales_incoming_rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
)
|
||||
|
||||
def validate_for_subcontracting(self):
|
||||
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
|
||||
@@ -566,11 +586,9 @@ class BuyingController(SubcontractingController):
|
||||
if d.from_warehouse:
|
||||
sle.dependant_sle_voucher_detail_no = d.name
|
||||
else:
|
||||
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
|
||||
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
|
||||
sle.update(
|
||||
{
|
||||
"incoming_rate": incoming_rate,
|
||||
"incoming_rate": d.valuation_rate,
|
||||
"recalculate_rate": 1
|
||||
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
|
||||
else 0,
|
||||
|
||||
@@ -319,6 +319,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
def set_missing_values(source, target):
|
||||
doc = frappe.get_doc(target)
|
||||
doc.is_return = 1
|
||||
doc.ignore_pricing_rule = 1
|
||||
doc.pricing_rules = []
|
||||
doc.return_against = source.name
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
@@ -478,6 +480,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
target_doc.pricing_rules = None
|
||||
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||
@@ -640,6 +643,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 +663,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},
|
||||
},
|
||||
|
||||
@@ -432,6 +432,9 @@ class SellingController(StockController):
|
||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||
return
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||
@@ -478,6 +481,9 @@ class SellingController(StockController):
|
||||
if d.incoming_rate != incoming_rate:
|
||||
d.incoming_rate = incoming_rate
|
||||
else:
|
||||
if allow_at_arms_length_price:
|
||||
continue
|
||||
|
||||
rate = flt(
|
||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
||||
d.precision("rate"),
|
||||
@@ -536,7 +542,9 @@ class SellingController(StockController):
|
||||
|
||||
def get_sle_for_source_warehouse(self, item_row):
|
||||
serial_and_batch_bundle = (
|
||||
item_row.serial_and_batch_bundle if not self.is_internal_transfer() else None
|
||||
item_row.serial_and_batch_bundle
|
||||
if not self.is_internal_transfer() or self.docstatus == 1
|
||||
else None
|
||||
)
|
||||
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
|
||||
if self.docstatus == 1:
|
||||
|
||||
@@ -93,6 +93,9 @@ class calculate_taxes_and_totals:
|
||||
self.doc.base_tax_withholding_net_total = sum_base_net_amount
|
||||
|
||||
def validate_item_tax_template(self):
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
if item.item_code and item.get("item_tax_template"):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
@@ -242,7 +245,6 @@ class calculate_taxes_and_totals:
|
||||
"tax_fraction_for_current_item",
|
||||
"grand_total_fraction_for_current_item",
|
||||
]
|
||||
|
||||
if tax.charge_type != "Actual" and not (
|
||||
self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total"
|
||||
):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
@@ -804,6 +805,41 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
@change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
|
||||
def test_16_internal_transfer_at_arms_length_price(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
target_warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company)
|
||||
warehouse = create_warehouse("_Test Internal Warehouse New 2", company=company)
|
||||
arms_length_price = 40
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
debit_to="Debtors - TCP1",
|
||||
target_warehouse=target_warehouse,
|
||||
warehouse=warehouse,
|
||||
income_account="Sales - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
update_stock=True,
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
si.items[0].rate = arms_length_price
|
||||
si.save()
|
||||
# rate should not reset to incoming rate
|
||||
self.assertEqual(si.items[0].rate, arms_length_price)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 0)
|
||||
si.items[0].rate = arms_length_price
|
||||
si.save()
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
|
||||
@@ -93,7 +93,26 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
|
||||
def onload(self):
|
||||
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
||||
|
||||
load_address_and_contact(ref_doc)
|
||||
load_address_and_contact(self)
|
||||
|
||||
ref_doc_contact_list = ref_doc.get("__onload").get("contact_list")
|
||||
opportunity_doc_contact_list = [
|
||||
contact
|
||||
for contact in self.get("__onload").get("contact_list")
|
||||
if contact not in ref_doc_contact_list
|
||||
]
|
||||
ref_doc_contact_list.extend(opportunity_doc_contact_list)
|
||||
ref_doc.set_onload("contact_list", ref_doc_contact_list)
|
||||
|
||||
ref_doc_addr_list = ref_doc.get("__onload").get("addr_list")
|
||||
opportunity_doc_addr_list = [
|
||||
addr for addr in self.get("__onload").get("addr_list") if addr not in ref_doc_addr_list
|
||||
]
|
||||
ref_doc_addr_list.extend(opportunity_doc_addr_list)
|
||||
ref_doc.set_onload("addr_list", ref_doc_addr_list)
|
||||
|
||||
self.set("__onload", ref_doc.get("__onload"))
|
||||
|
||||
def after_insert(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ from itertools import groupby
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
@@ -21,7 +21,15 @@ class SalesPipelineAnalytics:
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
|
||||
def validate_filters(self):
|
||||
if not self.filters.from_date:
|
||||
frappe.throw(_("From Date is mandatory"))
|
||||
|
||||
if not self.filters.to_date:
|
||||
frappe.throw(_("To Date is mandatory"))
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
self.get_columns()
|
||||
self.get_data()
|
||||
self.get_chart_data()
|
||||
@@ -185,7 +193,7 @@ class SalesPipelineAnalytics:
|
||||
count_or_amount = info.get(based_on)
|
||||
|
||||
if self.filters.get("pipeline_by") == "Owner":
|
||||
if value == "Not Assigned" or value == "[]" or value is None:
|
||||
if value == "Not Assigned" or value == "[]" or value is None or not value:
|
||||
assigned_to = ["Not Assigned"]
|
||||
else:
|
||||
assigned_to = json.loads(value)
|
||||
@@ -227,10 +235,9 @@ class SalesPipelineAnalytics:
|
||||
|
||||
def get_month_list(self):
|
||||
month_list = []
|
||||
current_date = date.today()
|
||||
month_number = date.today().month
|
||||
current_date = getdate(self.filters.get("from_date"))
|
||||
|
||||
for _month in range(month_number, 13):
|
||||
while current_date < getdate(self.filters.get("to_date")):
|
||||
month_list.append(current_date.strftime("%B"))
|
||||
current_date = current_date + relativedelta(months=1)
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute
|
||||
|
||||
|
||||
class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
class TestSalesPipelineAnalytics(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Opportunity")
|
||||
create_company()
|
||||
create_customer()
|
||||
create_opportunity()
|
||||
|
||||
def test_sales_pipeline_analytics(self):
|
||||
self.from_date = "2021-01-01"
|
||||
self.to_date = "2021-12-31"
|
||||
self.check_for_monthly_and_number()
|
||||
self.check_for_monthly_and_amount()
|
||||
self.check_for_quarterly_and_number()
|
||||
@@ -28,6 +30,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -43,6 +47,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -59,6 +65,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -74,6 +82,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -90,6 +100,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -105,6 +117,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -121,6 +135,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -136,6 +152,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"status": "Open",
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
@@ -153,8 +171,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||
"opportunity_type": "Sales",
|
||||
"company": "Best Test",
|
||||
"opportunity_source": "Cold Calling",
|
||||
"from_date": "2021-08-01",
|
||||
"to_date": "2021-08-31",
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
}
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
10
erpnext/gettext/extractors/incoterms.py
Normal file
10
erpnext/gettext/extractors/incoterms.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from csv import DictReader
|
||||
from io import StringIO
|
||||
|
||||
|
||||
def extract(fileobj, *args, **kwargs):
|
||||
"""Extract incoterm titles from a CSV file."""
|
||||
file = StringIO(fileobj.read().decode()) # CSV reader expects a text file
|
||||
reader = DictReader(file)
|
||||
for i, row in enumerate(reader):
|
||||
yield i + 2, "_", row["title"], ["Title of an incoterm"]
|
||||
@@ -449,6 +449,7 @@ scheduler_events = {
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
20314
erpnext/locale/eo.po
20314
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
19306
erpnext/locale/sv.po
19306
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
@@ -534,6 +534,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
quotation_to: me.frm.doc.quotation_to,
|
||||
supplier: me.frm.doc.supplier,
|
||||
currency: me.frm.doc.currency,
|
||||
is_internal_supplier: me.frm.doc.is_internal_supplier,
|
||||
is_internal_customer: me.frm.doc.is_internal_customer,
|
||||
update_stock: update_stock,
|
||||
conversion_rate: me.frm.doc.conversion_rate,
|
||||
price_list: me.frm.doc.selling_price_list || me.frm.doc.buying_price_list,
|
||||
@@ -826,47 +828,76 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
var me = this;
|
||||
var set_pricing = function() {
|
||||
if(me.frm.doc.company && me.frm.fields_dict.currency) {
|
||||
var company_currency = me.get_company_currency();
|
||||
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
|
||||
|
||||
if (!me.frm.doc.currency) {
|
||||
me.frm.set_value("currency", company_currency);
|
||||
}
|
||||
|
||||
if (me.frm.doc.currency == company_currency) {
|
||||
me.frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
if (me.frm.doc.price_list_currency == company_currency) {
|
||||
me.frm.set_value('plc_conversion_rate', 1.0);
|
||||
}
|
||||
if (company_doc){
|
||||
if (company_doc.default_letter_head) {
|
||||
if(me.frm.fields_dict.letter_head) {
|
||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
}
|
||||
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
|
||||
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
||||
selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
|
||||
me.frm.set_value("tc_name", company_doc.default_selling_terms);
|
||||
}
|
||||
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
|
||||
"Material Request", "Purchase Receipt"];
|
||||
// Purchase Invoice is excluded as per issue #3345
|
||||
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
||||
buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
|
||||
me.frm.set_value("tc_name", company_doc.default_buying_terms);
|
||||
}
|
||||
}
|
||||
frappe.run_serially([
|
||||
() => me.frm.script_manager.trigger("currency"),
|
||||
() => get_party_currency(),
|
||||
() => me.update_item_tax_map(),
|
||||
() => me.apply_default_taxes(),
|
||||
() => me.apply_pricing_rule()
|
||||
() => me.apply_pricing_rule(),
|
||||
() => set_terms(),
|
||||
() => set_letter_head(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
var get_party_currency = function() {
|
||||
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
|
||||
var party_name = me.frm.doc[party_type.toLowerCase()];
|
||||
if (party_name) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: party_type,
|
||||
filters: { name: party_name },
|
||||
fieldname: "default_currency",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
set_currency(r.message.default_currency);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
set_currency();
|
||||
}
|
||||
}
|
||||
|
||||
var set_currency = function(party_default_currency) {
|
||||
var company_currency = me.get_company_currency();
|
||||
var currency = party_default_currency || company_currency;
|
||||
if (me.frm.doc.currency != currency) {
|
||||
me.frm.set_value("currency", currency);
|
||||
}
|
||||
|
||||
if (me.frm.doc.currency == company_currency) {
|
||||
me.frm.set_value("conversion_rate", 1.0);
|
||||
}
|
||||
if (me.frm.doc.price_list_currency == company_currency) {
|
||||
me.frm.set_value('plc_conversion_rate', 1.0);
|
||||
}
|
||||
|
||||
me.frm.script_manager.trigger("currency");
|
||||
}
|
||||
|
||||
var set_terms = function() {
|
||||
if (frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && !me.frm.doc.tc_name) {
|
||||
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
|
||||
var selling_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"];
|
||||
var company_terms_fieldname = selling_doctypes.includes(me.frm.doc.doctype) ? "default_selling_terms" : "default_buying_terms";
|
||||
if (company_doc && company_doc[company_terms_fieldname]) {
|
||||
me.frm.set_value("tc_name", company_doc[company_terms_fieldname]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var set_letter_head = function() {
|
||||
if(me.frm.fields_dict.letter_head) {
|
||||
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
|
||||
if (company_doc && company_doc.default_letter_head) {
|
||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var set_party_account = function(set_pricing) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
|
||||
if(me.frm.doc.doctype=="Sales Invoice") {
|
||||
@@ -1631,7 +1662,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
|
||||
"conversion_factor": me.frm.doc.conversion_factor,
|
||||
"pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
|
||||
"coupon_code": me.frm.doc.coupon_code
|
||||
"coupon_code": me.frm.doc.coupon_code,
|
||||
"is_internal_supplier": me.frm.doc.is_internal_supplier,
|
||||
"is_internal_customer": me.frm.doc.is_internal_customer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1791,6 +1824,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", "");
|
||||
}
|
||||
@@ -1806,7 +1845,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([
|
||||
@@ -1953,6 +1992,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let item_rates = {};
|
||||
let item_tax_templates = {};
|
||||
|
||||
if (me.frm.doc.is_return && me.frm.doc.return_against) return;
|
||||
|
||||
$.each(this.frm.doc.items || [], function(i, item) {
|
||||
if (item.item_code) {
|
||||
// Use combination of name and item code in case same item is added multiple times
|
||||
|
||||
@@ -12,7 +12,6 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, get_datetime_str, today
|
||||
from frappe.utils.data import format_datetime
|
||||
from frappe.utils.file_manager import save_file
|
||||
|
||||
import erpnext
|
||||
|
||||
@@ -101,16 +100,15 @@ class ImportSupplierInvoice(Document):
|
||||
self.file_count += 1
|
||||
if pi_name:
|
||||
self.purchase_invoices_count += 1
|
||||
save_file(
|
||||
file_name,
|
||||
encoded_content,
|
||||
"Purchase Invoice",
|
||||
pi_name,
|
||||
folder=None,
|
||||
decode=False,
|
||||
is_private=0,
|
||||
df=None,
|
||||
)
|
||||
|
||||
file_doc = frappe.new_doc("File")
|
||||
file_doc.file_name = file_name
|
||||
file_doc.attached_to_doctype = "Purchase Invoice"
|
||||
file_doc.attached_to_name = pi_name
|
||||
file_doc.content = encoded_content
|
||||
file_doc.decode = False
|
||||
file_doc.is_private = False
|
||||
file_doc.insert(ignore_permissions=True)
|
||||
|
||||
def prepare_items_for_invoice(self, file_content, invoices_args):
|
||||
qty = 1
|
||||
|
||||
@@ -193,6 +193,9 @@ erpnext.SalesFunnel = class SalesFunnel {
|
||||
this.options.width = ($(this.elements.funnel_wrapper).width() * 2.0) / 3.0;
|
||||
this.options.height = (Math.sqrt(3) * this.options.width) / 2.0;
|
||||
|
||||
const min_height = (this.options.height * 0.1) / this.options.data.length;
|
||||
const height = this.options.height * 0.9;
|
||||
|
||||
// calculate total weightage
|
||||
// as height decreases, area decreases by the square of the reduction
|
||||
// hence, compensating by squaring the index value
|
||||
@@ -202,7 +205,7 @@ erpnext.SalesFunnel = class SalesFunnel {
|
||||
|
||||
// calculate height for each data
|
||||
$.each(this.options.data, function (i, d) {
|
||||
d.height = (me.options.height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage;
|
||||
d.height = (height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage + min_height;
|
||||
});
|
||||
|
||||
this.elements.canvas = $("<canvas></canvas>")
|
||||
|
||||
@@ -738,7 +738,7 @@
|
||||
"fieldname": "auto_err_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "Daily\nWeekly"
|
||||
"options": "Daily\nWeekly\nMonthly"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -792,7 +792,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
|
||||
|
||||
@@ -35,12 +35,11 @@ frappe.ui.form.on("Supplier Group", {
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
frm.set_intro(frm.doc.__islocal ? "" : __("There is nothing to edit."));
|
||||
frm.trigger("set_root_readonly");
|
||||
},
|
||||
set_root_readonly: function (frm) {
|
||||
if (!frm.doc.parent_supplier_group && !frm.doc.__islocal) {
|
||||
frm.trigger("set_read_only");
|
||||
if (!frm.doc.parent_supplier_group && !frm.is_new()) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(__("This is a root supplier group and cannot be edited."));
|
||||
} else {
|
||||
frm.set_intro(null);
|
||||
|
||||
@@ -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
|
||||
@@ -2032,6 +2032,40 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
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")
|
||||
|
||||
@@ -1925,9 +1925,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)
|
||||
@@ -3506,6 +3516,122 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
def test_internal_transfer_for_batch_items_with_cancel_use_serial_batch_fields(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", 1)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 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 11",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF11-BT-CANBIFST-.####"},
|
||||
)
|
||||
|
||||
serial_item_doc = make_item(
|
||||
"_Test Serial No Item For Stock Transfer Cancel Case 11",
|
||||
{"has_serial_no": 1, "serial_no_series": "USBF11-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=1,
|
||||
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": 1,
|
||||
},
|
||||
)
|
||||
|
||||
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=1,
|
||||
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": 1,
|
||||
},
|
||||
)
|
||||
|
||||
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", "auto_create_serial_and_batch_bundle_for_outward", 1)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"base_net_rate",
|
||||
"base_net_amount",
|
||||
"valuation_rate",
|
||||
"sales_incoming_rate",
|
||||
"item_tax_amount",
|
||||
"rm_supp_cost",
|
||||
"landed_cost_voucher_amount",
|
||||
@@ -1124,12 +1125,22 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Return Qty from Rejected Warehouse",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
|
||||
"fieldname": "sales_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Sales Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-28 09:48:24.448815",
|
||||
"modified": "2024-07-19 12:14:21.521466",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -88,6 +88,7 @@ class PurchaseReceiptItem(Document):
|
||||
return_qty_from_rejected_warehouse: DF.Check
|
||||
returned_qty: DF.Float
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_incoming_rate: DF.Currency
|
||||
sales_order: DF.Link | None
|
||||
sales_order_item: DF.Data | None
|
||||
sample_quantity: DF.Int
|
||||
|
||||
@@ -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()
|
||||
@@ -340,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)
|
||||
@@ -393,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):
|
||||
@@ -867,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
|
||||
|
||||
@@ -2188,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")
|
||||
|
||||
@@ -2214,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 @@
|
||||
"total_amount",
|
||||
"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",
|
||||
|
||||
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)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"allow_negative_stock",
|
||||
"show_barcode_field",
|
||||
"clean_description_html",
|
||||
"allow_internal_transfer_at_arms_length_price",
|
||||
"quality_inspection_settings_section",
|
||||
"action_if_quality_inspection_is_not_submitted",
|
||||
"column_break_23",
|
||||
@@ -434,12 +435,18 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.",
|
||||
"fieldname": "allow_internal_transfer_at_arms_length_price",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Internal Transfers at Arm's Length Price"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.valuation_method === \"Moving Average\"",
|
||||
@@ -460,7 +467,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-15 17:18:23.872161",
|
||||
"modified": "2024-07-29 14:55:19.093508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
||||
@@ -27,6 +27,7 @@ class StockSettings(Document):
|
||||
action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"]
|
||||
allow_from_dn: DF.Check
|
||||
allow_from_pr: DF.Check
|
||||
allow_internal_transfer_at_arms_length_price: DF.Check
|
||||
allow_negative_stock: DF.Check
|
||||
allow_partial_reservation: DF.Check
|
||||
allow_to_edit_stock_uom_qty_for_purchase: DF.Check
|
||||
|
||||
@@ -820,6 +820,9 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
if price_list_rate is None or frappe.db.get_single_value(
|
||||
"Stock Settings", "update_existing_price_list_rate"
|
||||
):
|
||||
if args.get("is_internal_supplier") or args.get("is_internal_customer"):
|
||||
return out
|
||||
|
||||
if args.price_list and args.rate:
|
||||
insert_item_price(args)
|
||||
|
||||
@@ -831,7 +834,11 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
|
||||
return out
|
||||
|
||||
if not out.price_list_rate and args.transaction_type == "buying":
|
||||
if (
|
||||
not args.get("is_internal_supplier")
|
||||
and not out.price_list_rate
|
||||
and args.transaction_type == "buying"
|
||||
):
|
||||
from erpnext.stock.doctype.item.item import get_last_purchase_details
|
||||
|
||||
out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate))
|
||||
@@ -1203,7 +1210,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}
|
||||
|
||||
@@ -1242,7 +1249,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:
|
||||
@@ -1260,10 +1267,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
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@ def get_columns(filters):
|
||||
"width": 150,
|
||||
"options": "Batch",
|
||||
},
|
||||
{
|
||||
"label": _("Expiry Date"),
|
||||
"fieldname": "expiry_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150},
|
||||
]
|
||||
)
|
||||
@@ -97,6 +103,7 @@ def get_batchwise_data_from_stock_ledger(filters):
|
||||
table.item_code,
|
||||
table.batch_no,
|
||||
table.warehouse,
|
||||
batch.expiry_date,
|
||||
Sum(table.actual_qty).as_("balance_qty"),
|
||||
)
|
||||
.where(table.is_cancelled == 0)
|
||||
@@ -127,6 +134,7 @@ def get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters):
|
||||
table.item_code,
|
||||
ch_table.batch_no,
|
||||
table.warehouse,
|
||||
batch.expiry_date,
|
||||
Sum(ch_table.qty).as_("balance_qty"),
|
||||
)
|
||||
.where((table.is_cancelled == 0) & (table.docstatus == 1))
|
||||
@@ -152,10 +160,14 @@ def get_query_based_on_filters(query, batch, table, filters):
|
||||
if filters.batch_no:
|
||||
query = query.where(batch.name == filters.batch_no)
|
||||
|
||||
if not filters.include_expired_batches:
|
||||
query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull()))
|
||||
if filters.to_date == today():
|
||||
query = query.where(batch.batch_qty > 0)
|
||||
if filters.to_date == today():
|
||||
if not filters.include_expired_batches:
|
||||
query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull()))
|
||||
|
||||
query = query.where(batch.batch_qty > 0)
|
||||
|
||||
else:
|
||||
query = query.where(table.posting_date <= filters.to_date)
|
||||
|
||||
if filters.warehouse:
|
||||
lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -157,6 +157,7 @@ def get_data(filters):
|
||||
{
|
||||
"serial_no": bundle_data.get("serial_no"),
|
||||
"valuation_rate": bundle_data.get("valuation_rate"),
|
||||
"qty": args.qty,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -465,10 +465,13 @@ class FIFOSlots:
|
||||
)
|
||||
)
|
||||
|
||||
for field in ["item_code", "warehouse"]:
|
||||
for field in ["item_code"]:
|
||||
if self.filters.get(field):
|
||||
query = query.where(bundle[field] == self.filters.get(field))
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
query = self.__get_warehouse_conditions(bundle, query)
|
||||
|
||||
bundle_wise_serial_nos = frappe._dict({})
|
||||
for bundle_name, serial_no in query.run():
|
||||
bundle_wise_serial_nos.setdefault(bundle_name, []).append(serial_no)
|
||||
|
||||
@@ -114,18 +114,23 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_warehouse_list(filters):
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
if not filters.get("warehouse"):
|
||||
return frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={"company": filters.get("company"), "is_group": 0},
|
||||
fields=["name"],
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = frappe.qb.from_(wh).select(wh.name).where(wh.is_group == 0)
|
||||
warehouse = frappe.qb.DocType("Warehouse")
|
||||
lft, rgt = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"])
|
||||
|
||||
user_permitted_warehouse = get_permitted_documents("Warehouse")
|
||||
if user_permitted_warehouse:
|
||||
query = query.where(wh.name.isin(set(user_permitted_warehouse)))
|
||||
elif filters.get("warehouse"):
|
||||
query = query.where(wh.name == filters.get("warehouse"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
return (
|
||||
frappe.qb.from_(warehouse)
|
||||
.select("name")
|
||||
.where((warehouse.lft >= lft) & (warehouse.rgt <= rgt))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
def add_warehouse_column(columns, warehouse_list):
|
||||
|
||||
@@ -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:
|
||||
@@ -324,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)
|
||||
@@ -338,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(
|
||||
@@ -351,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(
|
||||
@@ -915,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)
|
||||
|
||||
@@ -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."
|
||||
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)
|
||||
@@ -1124,6 +1150,7 @@ class SerialBatchCreation:
|
||||
self.item_name,
|
||||
self.description,
|
||||
"Active",
|
||||
voucher_no,
|
||||
self.batch_no,
|
||||
)
|
||||
)
|
||||
@@ -1142,6 +1169,7 @@ class SerialBatchCreation:
|
||||
"item_name",
|
||||
"description",
|
||||
"status",
|
||||
"purchase_document_no",
|
||||
"batch_no",
|
||||
]
|
||||
|
||||
|
||||
@@ -275,7 +275,9 @@ def repost_future_sle(
|
||||
)
|
||||
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():
|
||||
@@ -1588,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"
|
||||
|
||||
@@ -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": "Text Editor",
|
||||
"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",
|
||||
|
||||
@@ -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