mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-06 13:49:13 +00:00
Merge branch 'develop' into st31369
This commit is contained in:
16
.mergify.yml
16
.mergify.yml
@@ -11,7 +11,6 @@ pull_request_rules:
|
|||||||
- author!=deepeshgarg007
|
- author!=deepeshgarg007
|
||||||
- author!=frappe-pr-bot
|
- author!=frappe-pr-bot
|
||||||
- author!=mergify[bot]
|
- author!=mergify[bot]
|
||||||
|
|
||||||
- or:
|
- or:
|
||||||
- base=version-13
|
- base=version-13
|
||||||
- base=version-12
|
- base=version-12
|
||||||
@@ -24,7 +23,6 @@ pull_request_rules:
|
|||||||
message: |
|
message: |
|
||||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||||
|
|
||||||
- name: backport to develop
|
- name: backport to develop
|
||||||
conditions:
|
conditions:
|
||||||
- label="backport develop"
|
- label="backport develop"
|
||||||
@@ -34,7 +32,6 @@ pull_request_rules:
|
|||||||
- develop
|
- develop
|
||||||
assignees:
|
assignees:
|
||||||
- "{{ author }}"
|
- "{{ author }}"
|
||||||
|
|
||||||
- name: backport to version-14-hotfix
|
- name: backport to version-14-hotfix
|
||||||
conditions:
|
conditions:
|
||||||
- label="backport version-14-hotfix"
|
- label="backport version-14-hotfix"
|
||||||
@@ -44,7 +41,6 @@ pull_request_rules:
|
|||||||
- version-14-hotfix
|
- version-14-hotfix
|
||||||
assignees:
|
assignees:
|
||||||
- "{{ author }}"
|
- "{{ author }}"
|
||||||
|
|
||||||
- name: backport to version-15-hotfix
|
- name: backport to version-15-hotfix
|
||||||
conditions:
|
conditions:
|
||||||
- label="backport version-15-hotfix"
|
- label="backport version-15-hotfix"
|
||||||
@@ -54,18 +50,6 @@ pull_request_rules:
|
|||||||
- version-15-hotfix
|
- version-15-hotfix
|
||||||
assignees:
|
assignees:
|
||||||
- "{{ author }}"
|
- "{{ author }}"
|
||||||
|
|
||||||
- name: backport to version-13-hotfix
|
|
||||||
conditions:
|
|
||||||
- label="backport version-13-hotfix"
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- version-13-hotfix
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
|
|
||||||
|
|
||||||
- name: Automatic merge on CI success and review
|
- name: Automatic merge on CI success and review
|
||||||
conditions:
|
conditions:
|
||||||
- status-success=linters
|
- status-success=linters
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://erpnext.com">
|
<a href="https://frappe.io/erpnext">
|
||||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||||
</a>
|
</a>
|
||||||
<h2>ERPNext</h2>
|
<h2>ERPNext</h2>
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
|
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
|
||||||
-
|
-
|
||||||
<a href="https://erpnext.com">Website</a>
|
<a href="https://frappe.io/erpnext">Website</a>
|
||||||
-
|
-
|
||||||
<a href="https://docs.erpnext.com">Documentation</a>
|
<a href="https://docs.frappe.io/erpnext/">Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## ERPNext
|
## ERPNext
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
from frappe.utils import cint, cstr
|
from frappe.utils import add_to_date, cint, cstr, pretty_date
|
||||||
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -479,6 +479,7 @@ def get_account_autoname(account_number, account_name, company):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||||
|
_ensure_idle_system()
|
||||||
account = frappe.get_cached_doc("Account", name)
|
account = frappe.get_cached_doc("Account", name)
|
||||||
if not account:
|
if not account:
|
||||||
return
|
return
|
||||||
@@ -540,6 +541,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def merge_account(old, new):
|
def merge_account(old, new):
|
||||||
|
_ensure_idle_system()
|
||||||
# Validate properties before merging
|
# Validate properties before merging
|
||||||
new_account = frappe.get_cached_doc("Account", new)
|
new_account = frappe.get_cached_doc("Account", new)
|
||||||
old_account = frappe.get_cached_doc("Account", old)
|
old_account = frappe.get_cached_doc("Account", old)
|
||||||
@@ -593,3 +595,27 @@ def sync_update_account_number_in_child(
|
|||||||
|
|
||||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_idle_system():
|
||||||
|
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
|
||||||
|
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||||
|
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||||
|
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We also lock inserts to GL entry table with for_update here.
|
||||||
|
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
|
||||||
|
except frappe.QueryTimeoutError:
|
||||||
|
# wait=False fails immediately if there's an active transaction.
|
||||||
|
last_gl_update = add_to_date(None, seconds=-1)
|
||||||
|
|
||||||
|
if last_gl_update > add_to_date(None, minutes=-5):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
|
||||||
|
).format(pretty_date(last_gl_update)),
|
||||||
|
title=_("System In Use"),
|
||||||
|
)
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
frappe.flags.allocate_payment_amount = true;
|
frappe.flags.allocate_payment_amount = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validate: async function (frm) {
|
||||||
|
await frm.events.set_exchange_gain_loss_deduction(frm);
|
||||||
|
},
|
||||||
|
|
||||||
validate_company: (frm) => {
|
validate_company: (frm) => {
|
||||||
if (!frm.doc.company) {
|
if (!frm.doc.company) {
|
||||||
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
|
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
|
||||||
@@ -1837,8 +1841,6 @@ function prompt_for_missing_account(frm, account) {
|
|||||||
(values) => resolve(values?.[account]),
|
(values) => resolve(values?.[account]),
|
||||||
__("Please Specify Account")
|
__("Please Specify Account")
|
||||||
);
|
);
|
||||||
|
|
||||||
dialog.on_hide = () => resolve("");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1942,7 +1942,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||||
|
|
||||||
elif self.party_type in ("Supplier", "Employee"):
|
elif self.party_type in ("Supplier", "Customer"):
|
||||||
if paid_amount > total_negative_outstanding:
|
if paid_amount > total_negative_outstanding:
|
||||||
if total_negative_outstanding == 0:
|
if total_negative_outstanding == 0:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
@@ -3437,13 +3437,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
|||||||
"""Add loss on income discount in base currency."""
|
"""Add loss on income discount in base currency."""
|
||||||
precision = doc.precision("total")
|
precision = doc.precision("total")
|
||||||
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||||
|
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||||
|
|
||||||
pe.append(
|
pe.append(
|
||||||
"deductions",
|
"deductions",
|
||||||
{
|
{
|
||||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
"amount": flt(base_loss_on_income, precision),
|
"amount": flt(base_loss_on_income, precision) * positive_negative,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3455,6 +3456,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
|||||||
tax_discount_loss = {}
|
tax_discount_loss = {}
|
||||||
base_total_tax_loss = 0
|
base_total_tax_loss = 0
|
||||||
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||||
|
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||||
|
|
||||||
# The same account head could be used more than once
|
# The same account head could be used more than once
|
||||||
for tax in doc.get("taxes", []):
|
for tax in doc.get("taxes", []):
|
||||||
@@ -3477,7 +3479,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
|||||||
"account": account,
|
"account": account,
|
||||||
"cost_center": pe.cost_center
|
"cost_center": pe.cost_center
|
||||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
"amount": flt(loss, precision),
|
"amount": flt(loss, precision) * positive_negative,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,48 @@ class TestPaymentEntry(IntegrationTestCase):
|
|||||||
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
|
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
|
||||||
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
|
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
|
||||||
|
|
||||||
|
def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
|
||||||
|
pi = make_purchase_invoice(do_not_save=1)
|
||||||
|
create_payment_terms_template_with_discount()
|
||||||
|
pi.payment_terms_template = "Test Discount Template"
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
|
||||||
|
pi.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Service Tax",
|
||||||
|
"rate": 18,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||||
|
pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
|
||||||
|
|
||||||
|
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||||
|
self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
|
||||||
|
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
|
||||||
|
self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
|
||||||
|
self.assertEqual(pe_with_tax_loss.difference_amount, 0)
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||||
|
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||||
|
self.assertEqual(pe.payment_type, "Pay")
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 295.0)
|
||||||
|
self.assertEqual(pe.paid_amount, 265.5)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, -29.5)
|
||||||
|
self.assertEqual(pe.difference_amount, 0)
|
||||||
|
|
||||||
def test_payment_entry_against_payment_terms_with_discount(self):
|
def test_payment_entry_against_payment_terms_with_discount(self):
|
||||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||||
create_payment_terms_template_with_discount()
|
create_payment_terms_template_with_discount()
|
||||||
|
|||||||
@@ -1625,6 +1625,5 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"timeline_field": "customer",
|
"timeline_field": "customer",
|
||||||
"title_field": "customer_name",
|
"title_field": "customer_name",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 1
|
|
||||||
}
|
}
|
||||||
@@ -895,11 +895,19 @@ frappe.ui.form.on("Sales Invoice", {
|
|||||||
|
|
||||||
project: function (frm) {
|
project: function (frm) {
|
||||||
if (frm.doc.project) {
|
if (frm.doc.project) {
|
||||||
|
frappe.call({
|
||||||
|
method: "is_auto_fetch_timesheet_enabled",
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: function (r) {
|
||||||
|
if (cint(r.message)) {
|
||||||
frm.events.add_timesheet_data(frm, {
|
frm.events.add_timesheet_data(frm, {
|
||||||
project: frm.doc.project,
|
project: frm.doc.project,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async add_timesheet_data(frm, kwargs) {
|
async add_timesheet_data(frm, kwargs) {
|
||||||
if (kwargs === "Sales Invoice") {
|
if (kwargs === "Sales Invoice") {
|
||||||
|
|||||||
@@ -1096,16 +1096,17 @@ class SalesInvoice(SellingController):
|
|||||||
timesheet.billing_amount = ts_doc.total_billable_amount
|
timesheet.billing_amount = ts_doc.total_billable_amount
|
||||||
|
|
||||||
def update_timesheet_billing_for_project(self):
|
def update_timesheet_billing_for_project(self):
|
||||||
if self.timesheets:
|
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
|
||||||
|
self.add_timesheet_data()
|
||||||
|
else:
|
||||||
self.calculate_billing_amount_for_timesheet()
|
self.calculate_billing_amount_for_timesheet()
|
||||||
|
|
||||||
@frappe.whitelist(methods=["PUT"])
|
@frappe.whitelist()
|
||||||
def add_timesheet_data(self):
|
def is_auto_fetch_timesheet_enabled(self):
|
||||||
if not self.timesheets and self.project:
|
return frappe.db.get_single_value("Projects Settings", "fetch_timesheet_in_sales_invoice")
|
||||||
self._add_timesheet_data()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def _add_timesheet_data(self):
|
@frappe.whitelist()
|
||||||
|
def add_timesheet_data(self):
|
||||||
self.set("timesheets", [])
|
self.set("timesheets", [])
|
||||||
if self.project:
|
if self.project:
|
||||||
for data in get_projectwise_timesheet_data(self.project):
|
for data in get_projectwise_timesheet_data(self.project):
|
||||||
|
|||||||
@@ -4305,6 +4305,31 @@ class TestSalesInvoice(IntegrationTestCase):
|
|||||||
doc = frappe.get_doc("Project", project.name)
|
doc = frappe.get_doc("Project", project.name)
|
||||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||||
|
|
||||||
|
def test_total_billed_amount_with_different_projects(self):
|
||||||
|
# This test case is for checking the scenario where project is set at document level and for **some** child items only, not all
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
si = create_sales_invoice(do_not_submit=True)
|
||||||
|
|
||||||
|
project = frappe.new_doc("Project")
|
||||||
|
project.company = "_Test Company"
|
||||||
|
project.project_name = "Test Total Billed Amount"
|
||||||
|
project.save()
|
||||||
|
|
||||||
|
si.project = project.name
|
||||||
|
si.items.append(copy(si.items[0]))
|
||||||
|
si.items.append(copy(si.items[0]))
|
||||||
|
si.items[0].project = project.name
|
||||||
|
si.items[1].project = project.name
|
||||||
|
# Not setting project on last item
|
||||||
|
si.items[1].insert()
|
||||||
|
si.items[2].insert()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
project.reload()
|
||||||
|
self.assertIsNone(si.items[2].project)
|
||||||
|
self.assertEqual(project.total_billed_amount, 300)
|
||||||
|
|
||||||
def test_pos_returns_with_party_account_currency(self):
|
def test_pos_returns_with_party_account_currency(self):
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,15 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "voucher_type",
|
"fieldname": "voucher_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Voucher Type",
|
"label": "Voucher Type"
|
||||||
"options": "DocType"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "voucher_name",
|
"fieldname": "voucher_name",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Voucher Name",
|
"label": "Voucher Name"
|
||||||
"options": "voucher_type"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "taxable_amount",
|
"fieldname": "taxable_amount",
|
||||||
@@ -36,7 +34,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:52.307012",
|
"modified": "2025-02-05 16:39:14.863698",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Tax Withheld Vouchers",
|
"name": "Tax Withheld Vouchers",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
|
|||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
taxable_amount: DF.Currency
|
taxable_amount: DF.Currency
|
||||||
voucher_name: DF.DynamicLink | None
|
voucher_name: DF.Data | None
|
||||||
voucher_type: DF.Link | None
|
voucher_type: DF.Data | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
|||||||
tax_details.get("tax_withholding_category"),
|
tax_details.get("tax_withholding_category"),
|
||||||
company,
|
company,
|
||||||
),
|
),
|
||||||
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in journal_entries_details:
|
for d in journal_entries_details:
|
||||||
|
|||||||
@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, IntegrationTestCase):
|
|||||||
self.assertEqual(data[1][0].get("outstanding"), 300)
|
self.assertEqual(data[1][0].get("outstanding"), 300)
|
||||||
self.assertEqual(data[1][0].get("currency"), "USD")
|
self.assertEqual(data[1][0].get("currency"), "USD")
|
||||||
|
|
||||||
|
def test_account_payable_for_debit_note(self):
|
||||||
|
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||||
|
pi.is_return = 1
|
||||||
|
pi.items[0].qty = -1
|
||||||
|
pi = pi.save().submit()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Supplier",
|
||||||
|
"party": [self.supplier],
|
||||||
|
"report_date": today(),
|
||||||
|
"range": "30, 60, 90, 120",
|
||||||
|
}
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
self.assertEqual(data[1][0].get("invoiced"), 300)
|
||||||
|
|
||||||
def create_purchase_invoice(self, do_not_submit=False):
|
def create_purchase_invoice(self, do_not_submit=False):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
|
|||||||
@@ -267,6 +267,18 @@ class ReceivablePayableReport:
|
|||||||
row.invoiced_in_account_currency += amount_in_account_currency
|
row.invoiced_in_account_currency += amount_in_account_currency
|
||||||
else:
|
else:
|
||||||
if self.is_invoice(ple):
|
if self.is_invoice(ple):
|
||||||
|
# when invoice has is_return marked
|
||||||
|
if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
|
||||||
|
# for Credit Note
|
||||||
|
if row.voucher_type == "Sales Invoice":
|
||||||
|
row.credit_note -= amount
|
||||||
|
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||||
|
# for Debit Note
|
||||||
|
else:
|
||||||
|
row.invoiced -= amount
|
||||||
|
row.invoiced_in_account_currency -= amount_in_account_currency
|
||||||
|
return
|
||||||
|
|
||||||
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
||||||
row.paid -= amount
|
row.paid -= amount
|
||||||
row.paid_in_account_currency -= amount_in_account_currency
|
row.paid_in_account_currency -= amount_in_account_currency
|
||||||
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
|
|||||||
# nosemgrep
|
# nosemgrep
|
||||||
si_list = frappe.db.sql(
|
si_list = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select name, due_date, po_no
|
select name, due_date, po_no, is_return
|
||||||
from `tabSales Invoice`
|
from `tabSales Invoice`
|
||||||
where posting_date <= %s
|
where posting_date <= %s
|
||||||
and company = %s
|
and company = %s
|
||||||
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
|
|||||||
# nosemgrep
|
# nosemgrep
|
||||||
for pi in frappe.db.sql(
|
for pi in frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select name, due_date, bill_no, bill_date
|
select name, due_date, bill_no, bill_date, is_return
|
||||||
from `tabPurchase Invoice`
|
from `tabPurchase Invoice`
|
||||||
where
|
where
|
||||||
posting_date <= %s
|
posting_date <= %s
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
|||||||
|
|
||||||
expected_data_after_credit_note = [
|
expected_data_after_credit_note = [
|
||||||
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
|
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
|
||||||
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
|
[0, 0, 0, 100.0, -100.0, cr_note.name],
|
||||||
]
|
]
|
||||||
self.assertEqual(len(report[1]), 2)
|
self.assertEqual(len(report[1]), 2)
|
||||||
si_row = next(
|
si_row = next(
|
||||||
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
|||||||
report = execute(filters)[1]
|
report = execute(filters)[1]
|
||||||
self.assertEqual(len(report), 2)
|
self.assertEqual(len(report), 2)
|
||||||
|
|
||||||
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
|
expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
|
||||||
|
|
||||||
rows = report[:2]
|
rows = report[:2]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
expected_data[row.voucher_no],
|
expected_data[row.voucher_no],
|
||||||
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
[
|
||||||
|
row.invoiced or row.paid,
|
||||||
|
row.credit_note,
|
||||||
|
row.outstanding,
|
||||||
|
row.remaining_balance,
|
||||||
|
row.future_amount,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
pe.cancel()
|
pe.cancel()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
|
|||||||
["Purchase Invoice", "docstatus", "=", 1],
|
["Purchase Invoice", "docstatus", "=", 1],
|
||||||
["Purchase Invoice", "per_received", "<", 100],
|
["Purchase Invoice", "per_received", "<", 100],
|
||||||
["Purchase Invoice", "update_stock", "=", 0],
|
["Purchase Invoice", "update_stock", "=", 0],
|
||||||
|
["Purchase Invoice", "is_opening", "!=", "Yes"],
|
||||||
]
|
]
|
||||||
|
|
||||||
if report_filters.get("purchase_invoice"):
|
if report_filters.get("purchase_invoice"):
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
|
|||||||
and ba.account=gl.account
|
and ba.account=gl.account
|
||||||
and b.{budget_against} = gl.{budget_against}
|
and b.{budget_against} = gl.{budget_against}
|
||||||
and gl.fiscal_year between %s and %s
|
and gl.fiscal_year between %s and %s
|
||||||
|
and gl.is_cancelled = 0
|
||||||
and b.{budget_against} = %s
|
and b.{budget_against} = %s
|
||||||
and exists(
|
and exists(
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -652,6 +652,9 @@ frappe.ui.form.on("Asset", {
|
|||||||
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
||||||
frm.set_value("asset_quantity", data.asset_quantity);
|
frm.set_value("asset_quantity", data.asset_quantity);
|
||||||
frm.set_value("cost_center", data.cost_center);
|
frm.set_value("cost_center", data.cost_center);
|
||||||
|
if (data.asset_location) {
|
||||||
|
frm.set_value("location", data.asset_location);
|
||||||
|
}
|
||||||
|
|
||||||
if (doctype === "Purchase Receipt") {
|
if (doctype === "Purchase Receipt") {
|
||||||
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
|
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
|||||||
width: "80",
|
width: "80",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||||
|
on_change: (report) => {
|
||||||
|
report.set_filter_value("name", []);
|
||||||
|
report.refresh();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "to_date",
|
fieldname: "to_date",
|
||||||
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
|||||||
width: "80",
|
width: "80",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frappe.datetime.get_today(),
|
default: frappe.datetime.get_today(),
|
||||||
|
on_change: (report) => {
|
||||||
|
report.set_filter_value("name", []);
|
||||||
|
report.refresh();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "project",
|
fieldname: "project",
|
||||||
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
|||||||
{
|
{
|
||||||
fieldname: "name",
|
fieldname: "name",
|
||||||
label: __("Purchase Order"),
|
label: __("Purchase Order"),
|
||||||
fieldtype: "Link",
|
fieldtype: "MultiSelectList",
|
||||||
width: "80",
|
width: "80",
|
||||||
options: "Purchase Order",
|
options: "Purchase Order",
|
||||||
get_query: () => {
|
get_data: function (txt) {
|
||||||
return {
|
let filters = { docstatus: 1 };
|
||||||
filters: { docstatus: 1 },
|
|
||||||
};
|
const from_date = frappe.query_report.get_filter_value("from_date");
|
||||||
|
const to_date = frappe.query_report.get_filter_value("to_date");
|
||||||
|
if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
|
||||||
|
|
||||||
|
return frappe.db.get_link_options("Purchase Order", txt, filters);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -52,9 +64,16 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
|||||||
label: __("Status"),
|
label: __("Status"),
|
||||||
fieldtype: "MultiSelectList",
|
fieldtype: "MultiSelectList",
|
||||||
width: "80",
|
width: "80",
|
||||||
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"],
|
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed", "Closed"],
|
||||||
get_data: function (txt) {
|
get_data: function (txt) {
|
||||||
let status = ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"];
|
let status = [
|
||||||
|
"To Pay",
|
||||||
|
"To Bill",
|
||||||
|
"To Receive",
|
||||||
|
"To Receive and Bill",
|
||||||
|
"Completed",
|
||||||
|
"Closed",
|
||||||
|
];
|
||||||
let options = [];
|
let options = [];
|
||||||
for (let option of status) {
|
for (let option of status) {
|
||||||
options.push({
|
options.push({
|
||||||
|
|||||||
@@ -70,14 +70,16 @@ def get_data(filters):
|
|||||||
po.company,
|
po.company,
|
||||||
po_item.name,
|
po_item.name,
|
||||||
)
|
)
|
||||||
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1))
|
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
|
||||||
.groupby(po_item.name)
|
.groupby(po_item.name)
|
||||||
.orderby(po.transaction_date)
|
.orderby(po.transaction_date)
|
||||||
)
|
)
|
||||||
|
|
||||||
for field in ("company", "name"):
|
if filters.get("company"):
|
||||||
if filters.get(field):
|
query = query.where(po.company == filters.get("company"))
|
||||||
query = query.where(po[field] == filters.get(field))
|
|
||||||
|
if filters.get("name"):
|
||||||
|
query = query.where(po.name.isin(filters.get("name")))
|
||||||
|
|
||||||
if filters.get("from_date") and filters.get("to_date"):
|
if filters.get("from_date") and filters.get("to_date"):
|
||||||
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))
|
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))
|
||||||
|
|||||||
@@ -197,6 +197,14 @@ class AccountsController(TransactionBase):
|
|||||||
self.set_incoming_rate()
|
self.set_incoming_rate()
|
||||||
self.init_internal_values()
|
self.init_internal_values()
|
||||||
|
|
||||||
|
# Need to set taxes based on taxes_and_charges template
|
||||||
|
# before calculating taxes and totals
|
||||||
|
if self.meta.get_field("taxes_and_charges"):
|
||||||
|
self.validate_enabled_taxes_and_charges()
|
||||||
|
self.validate_tax_account_company()
|
||||||
|
|
||||||
|
self.set_taxes_and_charges()
|
||||||
|
|
||||||
if self.meta.get_field("currency"):
|
if self.meta.get_field("currency"):
|
||||||
self.calculate_taxes_and_totals()
|
self.calculate_taxes_and_totals()
|
||||||
|
|
||||||
@@ -207,10 +215,6 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
self.validate_all_documents_schedule()
|
self.validate_all_documents_schedule()
|
||||||
|
|
||||||
if self.meta.get_field("taxes_and_charges"):
|
|
||||||
self.validate_enabled_taxes_and_charges()
|
|
||||||
self.validate_tax_account_company()
|
|
||||||
|
|
||||||
self.validate_party()
|
self.validate_party()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
self.validate_party_account_currency()
|
self.validate_party_account_currency()
|
||||||
@@ -255,8 +259,6 @@ class AccountsController(TransactionBase):
|
|||||||
self.validate_deferred_income_expense_account()
|
self.validate_deferred_income_expense_account()
|
||||||
self.set_inter_company_account()
|
self.set_inter_company_account()
|
||||||
|
|
||||||
self.set_taxes_and_charges()
|
|
||||||
|
|
||||||
if self.doctype == "Purchase Invoice":
|
if self.doctype == "Purchase Invoice":
|
||||||
self.calculate_paid_amount()
|
self.calculate_paid_amount()
|
||||||
# apply tax withholding only if checked and applicable
|
# apply tax withholding only if checked and applicable
|
||||||
@@ -823,11 +825,15 @@ class AccountsController(TransactionBase):
|
|||||||
and item.get("use_serial_batch_fields")
|
and item.get("use_serial_batch_fields")
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
if fieldname == "batch_no" and not item.batch_no and not item.is_free_item:
|
|
||||||
item.set("rate", ret.get("rate"))
|
|
||||||
item.set("price_list_rate", ret.get("price_list_rate"))
|
|
||||||
item.set(fieldname, value)
|
item.set(fieldname, value)
|
||||||
|
|
||||||
|
if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
|
||||||
|
if ret.get("rate"):
|
||||||
|
item.set("rate", ret.get("rate"))
|
||||||
|
|
||||||
|
if not item.get("price_list_rate") and ret.get("price_list_rate"):
|
||||||
|
item.set("price_list_rate", ret.get("price_list_rate"))
|
||||||
|
|
||||||
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
|
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
|
||||||
fieldname
|
fieldname
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -935,6 +935,35 @@ class TestAccountsController(IntegrationTestCase):
|
|||||||
self.assertEqual(exc_je_for_si, [])
|
self.assertEqual(exc_je_for_si, [])
|
||||||
self.assertEqual(exc_je_for_pe, [])
|
self.assertEqual(exc_je_for_pe, [])
|
||||||
|
|
||||||
|
@IntegrationTestCase.change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
|
||||||
|
def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
|
||||||
|
# Create a Sales Taxes and Charges Template
|
||||||
|
if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
|
||||||
|
doc = frappe.new_doc("Sales Taxes and Charges Template")
|
||||||
|
doc.company = self.company
|
||||||
|
doc.title = "_Test Tax"
|
||||||
|
doc.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "Sales Expenses - _TC",
|
||||||
|
"description": "Test taxes",
|
||||||
|
"rate": 9,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
# Create a Sales Invoice
|
||||||
|
sinv = frappe.new_doc("Sales Invoice")
|
||||||
|
sinv.customer = self.customer
|
||||||
|
sinv.company = self.company
|
||||||
|
sinv.currency = "INR"
|
||||||
|
sinv.taxes_and_charges = "_Test Tax - _TC"
|
||||||
|
sinv.append("items", {"item_code": "_Test Item", "qty": 1, "rate": 50})
|
||||||
|
sinv.insert()
|
||||||
|
|
||||||
|
self.assertEqual(sinv.total_taxes_and_charges, 4.5)
|
||||||
|
|
||||||
def test_20_journal_against_sales_invoice(self):
|
def test_20_journal_against_sales_invoice(self):
|
||||||
# Invoice in Foreign Currency
|
# Invoice in Foreign Currency
|
||||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
|
|||||||
app_description = """ERP made simple"""
|
app_description = """ERP made simple"""
|
||||||
app_icon = "fa fa-th"
|
app_icon = "fa fa-th"
|
||||||
app_color = "#e74c3c"
|
app_color = "#e74c3c"
|
||||||
app_email = "info@erpnext.com"
|
app_email = "hello@frappe.io"
|
||||||
app_license = "GNU General Public License (v3)"
|
app_license = "GNU General Public License (v3)"
|
||||||
source_link = "https://github.com/frappe/erpnext"
|
source_link = "https://github.com/frappe/erpnext"
|
||||||
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
|
||||||
@@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
|
|||||||
default_mail_footer = """
|
default_mail_footer = """
|
||||||
<span>
|
<span>
|
||||||
Sent via
|
Sent via
|
||||||
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank">
|
<a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
|
||||||
ERPNext
|
ERPNext
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
1115
erpnext/locale/ar.po
1115
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
3021
erpnext/locale/bs.po
3021
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1115
erpnext/locale/de.po
1115
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
1115
erpnext/locale/eo.po
1115
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
1133
erpnext/locale/es.po
1133
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
2365
erpnext/locale/fa.po
2365
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
1119
erpnext/locale/fr.po
1119
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1113
erpnext/locale/hu.po
1113
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1113
erpnext/locale/pl.po
1113
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
1113
erpnext/locale/ru.po
1113
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
1149
erpnext/locale/sv.po
1149
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
1121
erpnext/locale/tr.po
1121
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
1309
erpnext/locale/zh.po
1309
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -403,3 +403,4 @@ erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
|||||||
erpnext.patches.v14_0.update_posting_datetime
|
erpnext.patches.v14_0.update_posting_datetime
|
||||||
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
|
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
|
||||||
erpnext.patches.v15_0.recalculate_amount_difference_field
|
erpnext.patches.v15_0.recalculate_amount_difference_field
|
||||||
|
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ def get_asset_depreciation_schedules_map():
|
|||||||
.orderby(ds.idx)
|
.orderby(ds.idx)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if len(records) > 20000:
|
||||||
|
frappe.db.auto_commit_on_many_writes = True
|
||||||
|
|
||||||
asset_depreciation_schedules_map = frappe._dict()
|
asset_depreciation_schedules_map = frappe._dict()
|
||||||
for d in records:
|
for d in records:
|
||||||
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)
|
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)
|
||||||
|
|||||||
@@ -322,17 +322,31 @@ class Project(Document):
|
|||||||
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
|
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
|
||||||
|
|
||||||
def update_billed_amount(self):
|
def update_billed_amount(self):
|
||||||
# nosemgrep
|
self.total_billed_amount = self.get_billed_amount_from_parent() + self.get_billed_amount_from_child()
|
||||||
|
|
||||||
|
def get_billed_amount_from_parent(self):
|
||||||
total_billed_amount = frappe.db.sql(
|
total_billed_amount = frappe.db.sql(
|
||||||
"""select sum(base_net_amount)
|
"""select sum(base_net_amount)
|
||||||
from `tabSales Invoice Item` si_item, `tabSales Invoice` si
|
from `tabSales Invoice` si join `tabSales Invoice Item` si_item on si_item.parent = si.name
|
||||||
where si_item.parent = si.name
|
where si_item.project is null
|
||||||
and if(si_item.project, si_item.project, si.project) = %s
|
and si.project is not null
|
||||||
|
and si.project = %s
|
||||||
and si.docstatus = 1""",
|
and si.docstatus = 1""",
|
||||||
self.name,
|
self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0
|
return total_billed_amount and total_billed_amount[0][0] or 0
|
||||||
|
|
||||||
|
def get_billed_amount_from_child(self):
|
||||||
|
total_billed_amount = frappe.db.sql(
|
||||||
|
"""select sum(base_net_amount)
|
||||||
|
from `tabSales Invoice Item`
|
||||||
|
where project = %s
|
||||||
|
and docstatus = 1""",
|
||||||
|
self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return total_billed_amount and total_billed_amount[0][0] or 0
|
||||||
|
|
||||||
def after_rename(self, old_name, new_name, merge=False):
|
def after_rename(self, old_name, new_name, merge=False):
|
||||||
if old_name == self.copied_from:
|
if old_name == self.copied_from:
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"timesheet_sb",
|
"timesheet_sb",
|
||||||
"ignore_workstation_time_overlap",
|
"ignore_workstation_time_overlap",
|
||||||
"ignore_user_time_overlap",
|
"ignore_user_time_overlap",
|
||||||
"ignore_employee_time_overlap"
|
"ignore_employee_time_overlap",
|
||||||
|
"fetch_timesheet_in_sales_invoice"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -33,11 +34,18 @@
|
|||||||
"fieldname": "ignore_employee_time_overlap",
|
"fieldname": "ignore_employee_time_overlap",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Employee Time Overlap"
|
"label": "Ignore Employee Time Overlap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Enabling the check box will fetch timesheet on select of a Project in Sales Invoice",
|
||||||
|
"fieldname": "fetch_timesheet_in_sales_invoice",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Fetch Timesheet in Sales Invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:21.984404",
|
"modified": "2025-02-13 23:01:27.321902",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Projects Settings",
|
"name": "Projects Settings",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ProjectsSettings(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
|
fetch_timesheet_in_sales_invoice: DF.Check
|
||||||
ignore_employee_time_overlap: DF.Check
|
ignore_employee_time_overlap: DF.Check
|
||||||
ignore_user_time_overlap: DF.Check
|
ignore_user_time_overlap: DF.Check
|
||||||
ignore_workstation_time_overlap: DF.Check
|
ignore_workstation_time_overlap: DF.Check
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class TestTimesheet(IntegrationTestCase):
|
|||||||
self.assertEqual(item.qty, 2.00)
|
self.assertEqual(item.qty, 2.00)
|
||||||
self.assertEqual(item.rate, 50.00)
|
self.assertEqual(item.rate, 50.00)
|
||||||
|
|
||||||
|
@IntegrationTestCase.change_settings("Projects Settings", {"fetch_timesheet_in_sales_invoice": 1})
|
||||||
def test_timesheet_billing_based_on_project(self):
|
def test_timesheet_billing_based_on_project(self):
|
||||||
emp = make_employee("test_employee_6@salary.com")
|
emp = make_employee("test_employee_6@salary.com")
|
||||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||||
@@ -62,7 +63,7 @@ class TestTimesheet(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
sales_invoice = create_sales_invoice(do_not_save=True)
|
sales_invoice = create_sales_invoice(do_not_save=True)
|
||||||
sales_invoice.project = project
|
sales_invoice.project = project
|
||||||
sales_invoice._add_timesheet_data()
|
sales_invoice.add_timesheet_data()
|
||||||
sales_invoice.submit()
|
sales_invoice.submit()
|
||||||
|
|
||||||
ts = frappe.get_doc("Timesheet", timesheet.name)
|
ts = frappe.get_doc("Timesheet", timesheet.name)
|
||||||
|
|||||||
@@ -1373,6 +1373,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
|
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
|
||||||
() => this.apply_pricing_rule(item, true)
|
() => this.apply_pricing_rule(item, true)
|
||||||
]);
|
]);
|
||||||
|
} else {
|
||||||
|
this.conversion_factor(doc, cdt, cdn, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ erpnext.landed_cost_taxes_and_charges = {
|
|||||||
"Income Account",
|
"Income Account",
|
||||||
"Expenses Included In Valuation",
|
"Expenses Included In Valuation",
|
||||||
"Expenses Included In Asset Valuation",
|
"Expenses Included In Asset Valuation",
|
||||||
|
"Expense Account",
|
||||||
|
"Direct Expense",
|
||||||
|
"Indirect Expense",
|
||||||
|
"Stock Received But Not Billed",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
|
|||||||
@@ -540,6 +540,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
has_batch_no: this.item.has_batch_no,
|
has_batch_no: this.item.has_batch_no,
|
||||||
qty: qty,
|
qty: qty,
|
||||||
based_on: based_on,
|
based_on: based_on,
|
||||||
|
posting_date: this.frm.doc.posting_date,
|
||||||
|
posting_time: this.frm.doc.posting_time,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{ address_line1 }}<br>
|
{{ address_line1 }}<br>
|
||||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
|
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
|
||||||
{% if country != "United States" %}{{ country }}{% endif -%}
|
{% if country != "United States" %}{{ country }}{% endif -%}
|
||||||
|
|||||||
@@ -2185,6 +2185,45 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
|
|||||||
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
|
def test_delivery_note_rate_on_change_of_warehouse(self):
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
|
item = make_item(
|
||||||
|
"_Test Batch Item for Delivery Note Rate",
|
||||||
|
{
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BH-SDDTBIFRM-.#####",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
|
||||||
|
so = make_sales_order(
|
||||||
|
item_code=item.name, rate=27648.00, price_list_rate=27648.00, qty=1, do_not_submit=True
|
||||||
|
)
|
||||||
|
|
||||||
|
so.items[0].rate = 90
|
||||||
|
so.save()
|
||||||
|
self.assertTrue(so.items[0].discount_amount == 27558.0)
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
warehouse = create_warehouse("NW Warehouse FOR Rate", company=so.company)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item.name,
|
||||||
|
qty=2,
|
||||||
|
target=warehouse,
|
||||||
|
basic_rate=100,
|
||||||
|
company=so.company,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
dn = make_delivery_note(so.name)
|
||||||
|
dn.items[0].warehouse = warehouse
|
||||||
|
dn.save()
|
||||||
|
|
||||||
|
self.assertEqual(dn.items[0].rate, 90)
|
||||||
|
|
||||||
def test_credit_limit_on_so_reopning(self):
|
def test_credit_limit_on_so_reopning(self):
|
||||||
# set credit limit
|
# set credit limit
|
||||||
company = "_Test Company"
|
company = "_Test Company"
|
||||||
|
|||||||
@@ -952,7 +952,6 @@
|
|||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_width": "120px",
|
"print_width": "120px",
|
||||||
"reqd": 1,
|
|
||||||
"width": "120px"
|
"width": "120px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -971,7 +970,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-06 13:29:24.619850",
|
"modified": "2025-02-28 09:45:43.934947",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class SalesOrderItem(Document):
|
|||||||
brand: DF.Link | None
|
brand: DF.Link | None
|
||||||
company_total_stock: DF.Float
|
company_total_stock: DF.Float
|
||||||
conversion_factor: DF.Float
|
conversion_factor: DF.Float
|
||||||
cost_center: DF.Link
|
cost_center: DF.Link | None
|
||||||
customer_item_code: DF.Data | None
|
customer_item_code: DF.Data | None
|
||||||
delivered_by_supplier: DF.Check
|
delivered_by_supplier: DF.Check
|
||||||
delivered_qty: DF.Float
|
delivered_qty: DF.Float
|
||||||
|
|||||||
@@ -320,13 +320,13 @@ def get_past_order_list(search_term, status, limit=20):
|
|||||||
invoice_list = []
|
invoice_list = []
|
||||||
|
|
||||||
if search_term and status:
|
if search_term and status:
|
||||||
invoices_by_customer = frappe.db.get_all(
|
invoices_by_customer = frappe.db.get_list(
|
||||||
"POS Invoice",
|
"POS Invoice",
|
||||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||||
fields=fields,
|
fields=fields,
|
||||||
page_length=limit,
|
page_length=limit,
|
||||||
)
|
)
|
||||||
invoices_by_name = frappe.db.get_all(
|
invoices_by_name = frappe.db.get_list(
|
||||||
"POS Invoice",
|
"POS Invoice",
|
||||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
||||||
fields=fields,
|
fields=fields,
|
||||||
@@ -335,7 +335,7 @@ def get_past_order_list(search_term, status, limit=20):
|
|||||||
|
|
||||||
invoice_list = invoices_by_customer + invoices_by_name
|
invoice_list = invoices_by_customer + invoices_by_name
|
||||||
elif status:
|
elif status:
|
||||||
invoice_list = frappe.db.get_all(
|
invoice_list = frappe.db.get_list(
|
||||||
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
|
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
label: __("Opening Amount"),
|
label: __("Opening Amount"),
|
||||||
options: "company:company_currency",
|
options: "company:company_currency",
|
||||||
change: function () {
|
onchange: function () {
|
||||||
dialog.fields_dict.balance_details.df.data.some((d) => {
|
dialog.fields_dict.balance_details.df.data.some((d) => {
|
||||||
if (d.idx == this.doc.idx) {
|
if (d.idx == this.doc.idx) {
|
||||||
d.opening_amount = this.value;
|
d.opening_amount = this.value;
|
||||||
@@ -609,6 +609,14 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
|
|
||||||
if (this.is_current_item_being_edited(item_row) || from_selector) {
|
if (this.is_current_item_being_edited(item_row) || from_selector) {
|
||||||
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
|
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
|
||||||
|
if (item.serial_no && from_selector) {
|
||||||
|
await frappe.model.set_value(
|
||||||
|
item_row.doctype,
|
||||||
|
item_row.name,
|
||||||
|
"serial_no",
|
||||||
|
item_row.serial_no + `\n${item.serial_no}`
|
||||||
|
);
|
||||||
|
}
|
||||||
this.update_cart_html(item_row);
|
this.update_cart_html(item_row);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
this[`${fieldname}_control`].set_value(item[fieldname]);
|
this[`${fieldname}_control`].set_value(item[fieldname]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.resize_serial_control(item);
|
||||||
this.make_auto_serial_selection_btn(item);
|
this.make_auto_serial_selection_btn(item);
|
||||||
|
|
||||||
this.bind_custom_control_change_event();
|
this.bind_custom_control_change_event();
|
||||||
@@ -203,11 +204,17 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
"actual_qty",
|
"actual_qty",
|
||||||
"price_list_rate",
|
"price_list_rate",
|
||||||
];
|
];
|
||||||
if (item.has_serial_no) fields.push("serial_no");
|
if (item.has_serial_no || item.serial_no) fields.push("serial_no");
|
||||||
if (item.has_batch_no) fields.push("batch_no");
|
if (item.has_batch_no || item.batch_no) fields.push("batch_no");
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resize_serial_control(item) {
|
||||||
|
if (item.has_serial_no || item.serial_no) {
|
||||||
|
this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
make_auto_serial_selection_btn(item) {
|
make_auto_serial_selection_btn(item) {
|
||||||
if (item.has_serial_no || item.has_batch_no) {
|
if (item.has_serial_no || item.has_batch_no) {
|
||||||
if (item.has_serial_no && item.has_batch_no) {
|
if (item.has_serial_no && item.has_batch_no) {
|
||||||
@@ -225,7 +232,6 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
`<div class="btn btn-sm btn-secondary auto-fetch-btn" style="margin-top: 6px">${label}</div>`
|
`<div class="btn btn-sm btn-secondary auto-fetch-btn" style="margin-top: 6px">${label}</div>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -340,19 +340,11 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
// pass
|
// pass
|
||||||
}
|
}
|
||||||
|
|
||||||
async render_payment_section() {
|
render_payment_section() {
|
||||||
this.render_payment_mode_dom();
|
this.render_payment_mode_dom();
|
||||||
this.make_invoice_fields_control();
|
this.make_invoice_fields_control();
|
||||||
this.update_totals_section();
|
this.update_totals_section();
|
||||||
let r = await frappe.db.get_value(
|
this.unset_grand_total_to_default_mop();
|
||||||
"POS Profile",
|
|
||||||
this.frm.doc.pos_profile,
|
|
||||||
"disable_grand_total_to_default_mop"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!r.message.disable_grand_total_to_default_mop) {
|
|
||||||
this.focus_on_default_mop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
after_render() {
|
after_render() {
|
||||||
@@ -637,6 +629,19 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unset_grand_total_to_default_mop() {
|
||||||
|
const doc = this.events.get_frm().doc;
|
||||||
|
let r = await frappe.db.get_value(
|
||||||
|
"POS Profile",
|
||||||
|
doc.pos_profile,
|
||||||
|
"disable_grand_total_to_default_mop"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!r.message.disable_grand_total_to_default_mop) {
|
||||||
|
this.focus_on_default_mop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validate_reqd_invoice_fields() {
|
validate_reqd_invoice_fields() {
|
||||||
const doc = this.events.get_frm().doc;
|
const doc = this.events.get_frm().doc;
|
||||||
let validation_flag = true;
|
let validation_flag = true;
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ frappe.query_reports["Sales Order Analysis"] = {
|
|||||||
width: "80",
|
width: "80",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||||
|
on_change: (report) => {
|
||||||
|
report.set_filter_value("sales_order", []);
|
||||||
|
report.refresh();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "to_date",
|
fieldname: "to_date",
|
||||||
@@ -27,6 +31,10 @@ frappe.query_reports["Sales Order Analysis"] = {
|
|||||||
width: "80",
|
width: "80",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frappe.datetime.get_today(),
|
default: frappe.datetime.get_today(),
|
||||||
|
on_change: (report) => {
|
||||||
|
report.set_filter_value("sales_order", []);
|
||||||
|
report.refresh();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "sales_order",
|
fieldname: "sales_order",
|
||||||
@@ -35,12 +43,13 @@ frappe.query_reports["Sales Order Analysis"] = {
|
|||||||
width: "80",
|
width: "80",
|
||||||
options: "Sales Order",
|
options: "Sales Order",
|
||||||
get_data: function (txt) {
|
get_data: function (txt) {
|
||||||
return frappe.db.get_link_options("Sales Order", txt);
|
let filters = { docstatus: 1 };
|
||||||
},
|
|
||||||
get_query: () => {
|
const from_date = frappe.query_report.get_filter_value("from_date");
|
||||||
return {
|
const to_date = frappe.query_report.get_filter_value("to_date");
|
||||||
filters: { docstatus: 1 },
|
if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
|
||||||
};
|
|
||||||
|
return frappe.db.get_link_options("Sales Order", txt, filters);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -53,10 +62,17 @@ frappe.query_reports["Sales Order Analysis"] = {
|
|||||||
fieldname: "status",
|
fieldname: "status",
|
||||||
label: __("Status"),
|
label: __("Status"),
|
||||||
fieldtype: "MultiSelectList",
|
fieldtype: "MultiSelectList",
|
||||||
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"],
|
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed", "Closed"],
|
||||||
width: "80",
|
width: "80",
|
||||||
get_data: function (txt) {
|
get_data: function (txt) {
|
||||||
let status = ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"];
|
let status = [
|
||||||
|
"To Pay",
|
||||||
|
"To Bill",
|
||||||
|
"To Deliver",
|
||||||
|
"To Deliver and Bill",
|
||||||
|
"Completed",
|
||||||
|
"Closed",
|
||||||
|
];
|
||||||
let options = [];
|
let options = [];
|
||||||
for (let option of status) {
|
for (let option of status) {
|
||||||
options.push({
|
options.push({
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def get_data(conditions, filters):
|
|||||||
ON sii.so_detail = soi.name and sii.docstatus = 1
|
ON sii.so_detail = soi.name and sii.docstatus = 1
|
||||||
WHERE
|
WHERE
|
||||||
soi.parent = so.name
|
soi.parent = so.name
|
||||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
and so.status not in ('Stopped', 'On Hold')
|
||||||
and so.docstatus = 1
|
and so.docstatus = 1
|
||||||
{conditions}
|
{conditions}
|
||||||
GROUP BY soi.name
|
GROUP BY soi.name
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
|
|||||||
from .default_success_action import get_default_success_action
|
from .default_success_action import get_default_success_action
|
||||||
|
|
||||||
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
|
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
|
||||||
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
|
<a style="color: #888" href="http://frappe.io/erpnext">ERPNext</a></div>"""
|
||||||
|
|
||||||
|
|
||||||
def after_install():
|
def after_install():
|
||||||
|
|||||||
@@ -365,6 +365,8 @@ class DeprecatedBatchNoValuation:
|
|||||||
if self.sle.voucher_detail_no:
|
if self.sle.voucher_detail_no:
|
||||||
query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no)
|
query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no)
|
||||||
|
|
||||||
|
query = query.where(sabb.voucher_type != "Pick List")
|
||||||
|
|
||||||
data = query.run(as_dict=True)
|
data = query.run(as_dict=True)
|
||||||
if not data:
|
if not data:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ def update_qty(bin_name, args):
|
|||||||
& (sle.warehouse == args.get("warehouse"))
|
& (sle.warehouse == args.get("warehouse"))
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
)
|
)
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
|
.orderby(sle.posting_datetime, order=Order.desc)
|
||||||
.orderby(sle.creation, order=Order.desc)
|
.orderby(sle.creation, order=Order.desc)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -236,7 +236,12 @@ class InventoryDimension(Document):
|
|||||||
custom_fields[dt] = dimension_field
|
custom_fields[dt] = dimension_field
|
||||||
|
|
||||||
filter_custom_fields = {}
|
filter_custom_fields = {}
|
||||||
ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"]
|
ignore_doctypes = [
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
"Pick List Item",
|
||||||
|
"Maintenance Visit Purpose",
|
||||||
|
]
|
||||||
|
|
||||||
if custom_fields:
|
if custom_fields:
|
||||||
for doctype, fields in custom_fields.items():
|
for doctype, fields in custom_fields.items():
|
||||||
|
|||||||
@@ -1081,15 +1081,19 @@ def get_billed_amount_against_po(po_items):
|
|||||||
if not po_items:
|
if not po_items:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||||
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(purchase_invoice_item)
|
frappe.qb.from_(purchase_invoice_item)
|
||||||
|
.inner_join(purchase_invoice)
|
||||||
|
.on(purchase_invoice_item.parent == purchase_invoice.name)
|
||||||
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
|
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
|
||||||
.where(
|
.where(
|
||||||
(purchase_invoice_item.po_detail.isin(po_items))
|
(purchase_invoice_item.po_detail.isin(po_items))
|
||||||
& (purchase_invoice_item.docstatus == 1)
|
& (purchase_invoice.docstatus == 1)
|
||||||
& (purchase_invoice_item.pr_detail.isnull())
|
& (purchase_invoice_item.pr_detail.isnull())
|
||||||
|
& (purchase_invoice.update_stock == 0)
|
||||||
)
|
)
|
||||||
.groupby(purchase_invoice_item.po_detail)
|
.groupby(purchase_invoice_item.po_detail)
|
||||||
).run(as_dict=1)
|
).run(as_dict=1)
|
||||||
|
|||||||
@@ -4096,6 +4096,36 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
|||||||
batch_return.save()
|
batch_return.save()
|
||||||
batch_return.submit()
|
batch_return.submit()
|
||||||
|
|
||||||
|
def test_pr_status_based_on_invoices_with_update_stock(self):
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||||
|
make_purchase_invoice as _make_purchase_invoice,
|
||||||
|
)
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||||
|
make_purchase_receipt as _make_purchase_receipt,
|
||||||
|
)
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||||
|
create_pr_against_po,
|
||||||
|
create_purchase_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
item_code = "Test Item for PR Status Based on Invoices"
|
||||||
|
create_item(item_code)
|
||||||
|
|
||||||
|
po = create_purchase_order(item_code=item_code, qty=10)
|
||||||
|
pi = _make_purchase_invoice(po.name)
|
||||||
|
pi.update_stock = 1
|
||||||
|
pi.items[0].qty = 5
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.per_billed, 50)
|
||||||
|
|
||||||
|
pr = _make_purchase_receipt(po.name)
|
||||||
|
self.assertEqual(pr.items[0].qty, 5)
|
||||||
|
pr.submit()
|
||||||
|
pr.reload()
|
||||||
|
self.assertEqual(pr.status, "To Bill")
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"shipment_parcel",
|
"shipment_parcel",
|
||||||
"parcel_template",
|
"parcel_template",
|
||||||
"add_template",
|
"add_template",
|
||||||
|
"total_weight",
|
||||||
"column_break_28",
|
"column_break_28",
|
||||||
"shipment_delivery_note",
|
"shipment_delivery_note",
|
||||||
"shipment_details_section",
|
"shipment_details_section",
|
||||||
@@ -429,11 +430,17 @@
|
|||||||
"label": "Pickup Contact Person",
|
"label": "Pickup Contact Person",
|
||||||
"mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'",
|
"mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'",
|
||||||
"options": "User"
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_weight",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Total Weight (kg)",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:41.030764",
|
"modified": "2025-02-20 16:55:20.076418",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Shipment",
|
"name": "Shipment",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class Shipment(Document):
|
|||||||
shipment_parcel: DF.Table[ShipmentParcel]
|
shipment_parcel: DF.Table[ShipmentParcel]
|
||||||
shipment_type: DF.Literal["Goods", "Documents"]
|
shipment_type: DF.Literal["Goods", "Documents"]
|
||||||
status: DF.Literal["Draft", "Submitted", "Booked", "Cancelled", "Completed"]
|
status: DF.Literal["Draft", "Submitted", "Booked", "Cancelled", "Completed"]
|
||||||
|
total_weight: DF.Float
|
||||||
tracking_status: DF.Literal["", "In Progress", "Delivered", "Returned", "Lost"]
|
tracking_status: DF.Literal["", "In Progress", "Delivered", "Returned", "Lost"]
|
||||||
tracking_status_info: DF.Data | None
|
tracking_status_info: DF.Data | None
|
||||||
tracking_url: DF.SmallText | None
|
tracking_url: DF.SmallText | None
|
||||||
@@ -75,6 +76,7 @@ class Shipment(Document):
|
|||||||
self.validate_weight()
|
self.validate_weight()
|
||||||
self.validate_pickup_time()
|
self.validate_pickup_time()
|
||||||
self.set_value_of_goods()
|
self.set_value_of_goods()
|
||||||
|
self.set_total_weight()
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.status = "Draft"
|
self.status = "Draft"
|
||||||
|
|
||||||
@@ -93,6 +95,12 @@ class Shipment(Document):
|
|||||||
if flt(parcel.weight) <= 0:
|
if flt(parcel.weight) <= 0:
|
||||||
frappe.throw(_("Parcel weight cannot be 0"))
|
frappe.throw(_("Parcel weight cannot be 0"))
|
||||||
|
|
||||||
|
def set_total_weight(self):
|
||||||
|
self.total_weight = self.get_total_weight()
|
||||||
|
|
||||||
|
def get_total_weight(self):
|
||||||
|
return sum(flt(parcel.weight) * parcel.count for parcel in self.shipment_parcel if parcel.count > 0)
|
||||||
|
|
||||||
def validate_pickup_time(self):
|
def validate_pickup_time(self):
|
||||||
if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
|
if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
|
||||||
frappe.throw(_("Pickup To time should be greater than Pickup From time"))
|
frappe.throw(_("Pickup To time should be greater than Pickup From time"))
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ class TestShipment(IntegrationTestCase):
|
|||||||
self.assertEqual(len(second_shipment.shipment_delivery_note), 1)
|
self.assertEqual(len(second_shipment.shipment_delivery_note), 1)
|
||||||
self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name)
|
self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name)
|
||||||
|
|
||||||
|
def test_get_total_weight(self):
|
||||||
|
shipment = frappe.new_doc("Shipment")
|
||||||
|
shipment.extend(
|
||||||
|
"shipment_parcel",
|
||||||
|
[
|
||||||
|
{"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5},
|
||||||
|
{"length": 5, "width": 5, "height": 5, "weight": 10, "count": 1},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(shipment.get_total_weight(), 35)
|
||||||
|
|
||||||
|
|
||||||
def create_test_delivery_note():
|
def create_test_delivery_note():
|
||||||
company = get_shipment_company()
|
company = get_shipment_company()
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
|
||||||
|
on_doctype_update as create_sle_indexes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""Ensure SLE Indexes"""
|
||||||
|
|
||||||
|
create_sle_indexes()
|
||||||
@@ -47,18 +47,20 @@
|
|||||||
"auto_reserve_serial_and_batch",
|
"auto_reserve_serial_and_batch",
|
||||||
"serial_and_batch_item_settings_tab",
|
"serial_and_batch_item_settings_tab",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
|
"allow_existing_serial_no",
|
||||||
"do_not_use_batchwise_valuation",
|
"do_not_use_batchwise_valuation",
|
||||||
"auto_create_serial_and_batch_bundle_for_outward",
|
"auto_create_serial_and_batch_bundle_for_outward",
|
||||||
"pick_serial_and_batch_based_on",
|
"pick_serial_and_batch_based_on",
|
||||||
"naming_series_prefix",
|
|
||||||
"column_break_mhzc",
|
"column_break_mhzc",
|
||||||
"disable_serial_no_and_batch_selector",
|
"disable_serial_no_and_batch_selector",
|
||||||
"use_naming_series",
|
|
||||||
"use_serial_batch_fields",
|
"use_serial_batch_fields",
|
||||||
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
||||||
"allow_existing_serial_no",
|
|
||||||
"serial_and_batch_bundle_section",
|
"serial_and_batch_bundle_section",
|
||||||
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||||
|
"section_break_gnhq",
|
||||||
|
"use_naming_series",
|
||||||
|
"column_break_wslv",
|
||||||
|
"naming_series_prefix",
|
||||||
"stock_planning_tab",
|
"stock_planning_tab",
|
||||||
"auto_material_request",
|
"auto_material_request",
|
||||||
"auto_indent",
|
"auto_indent",
|
||||||
@@ -488,6 +490,14 @@
|
|||||||
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
|
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
|
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_gnhq",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wslv",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -495,7 +505,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-17 13:36:36.177743",
|
"modified": "2025-02-28 15:08:35.938840",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ def update_stock(ctx, out, doc=None):
|
|||||||
else:
|
else:
|
||||||
qty -= batch_qty
|
qty -= batch_qty
|
||||||
|
|
||||||
out.update({"batch_no": batch_no, "actual_batch_qty": qty})
|
out.update({"batch_no": batch_no, "actual_batch_qty": batch_qty})
|
||||||
if rate:
|
if rate:
|
||||||
out.update({"rate": rate, "price_list_rate": rate})
|
out.update({"rate": rate, "price_list_rate": rate})
|
||||||
|
|
||||||
@@ -1095,7 +1095,9 @@ def get_batch_based_item_price(pctx: ItemPriceCtx | dict | str, item_code) -> fl
|
|||||||
if not item_price:
|
if not item_price:
|
||||||
item_price = get_item_price(pctx, item_code, ignore_party=True, force_batch_no=True)
|
item_price = get_item_price(pctx, item_code, ignore_party=True, force_batch_no=True)
|
||||||
|
|
||||||
if item_price and item_price[0].uom == pctx.uom:
|
is_free_item = pctx.get("items", [{}])[0].get("is_free_item")
|
||||||
|
|
||||||
|
if item_price and item_price[0].uom == pctx.uom and not is_free_item:
|
||||||
return item_price[0].price_list_rate
|
return item_price[0].price_list_rate
|
||||||
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
|||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch_package = frappe.qb.DocType("Serial and Batch Entry")
|
batch_package = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
|
|
||||||
to_date = get_datetime(filters.to_date + " 23:59:59")
|
to_date = get_datetime(str(filters.to_date) + " 23:59:59")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
|
|||||||
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
||||||
range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
|
range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
|
||||||
|
|
||||||
|
check_and_replace_valuations_if_moving_average(
|
||||||
|
range_values, details.valuation_method, details.valuation_rate
|
||||||
|
)
|
||||||
|
|
||||||
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
|
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
|
||||||
|
|
||||||
if filters.get("show_warehouse_wise_stock"):
|
if filters.get("show_warehouse_wise_stock"):
|
||||||
@@ -72,6 +76,15 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def check_and_replace_valuations_if_moving_average(range_values, item_valuation_method, valuation_rate):
|
||||||
|
if item_valuation_method == "Moving Average" or (
|
||||||
|
not item_valuation_method
|
||||||
|
and frappe.db.get_single_value("Stock Settings", "valuation_method") == "Moving Average"
|
||||||
|
):
|
||||||
|
for i in range(0, len(range_values), 2):
|
||||||
|
range_values[i + 1] = range_values[i] * valuation_rate
|
||||||
|
|
||||||
|
|
||||||
def get_average_age(fifo_queue: list, to_date: str) -> float:
|
def get_average_age(fifo_queue: list, to_date: str) -> float:
|
||||||
batch_age = age_qty = total_qty = 0.0
|
batch_age = age_qty = total_qty = 0.0
|
||||||
for batch in fifo_queue:
|
for batch in fifo_queue:
|
||||||
@@ -396,6 +409,7 @@ class FIFOSlots:
|
|||||||
self.item_details[key]["total_qty"] += row.actual_qty
|
self.item_details[key]["total_qty"] += row.actual_qty
|
||||||
|
|
||||||
self.item_details[key]["has_serial_no"] = row.has_serial_no
|
self.item_details[key]["has_serial_no"] = row.has_serial_no
|
||||||
|
self.item_details[key]["details"].valuation_rate = row.valuation_rate
|
||||||
|
|
||||||
def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
|
def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
|
||||||
"Aggregate Item-Wh wise data into single Item entry."
|
"Aggregate Item-Wh wise data into single Item entry."
|
||||||
@@ -437,8 +451,10 @@ class FIFOSlots:
|
|||||||
item.description,
|
item.description,
|
||||||
item.stock_uom,
|
item.stock_uom,
|
||||||
item.has_serial_no,
|
item.has_serial_no,
|
||||||
|
item.valuation_method,
|
||||||
sle.actual_qty,
|
sle.actual_qty,
|
||||||
sle.stock_value_difference,
|
sle.stock_value_difference,
|
||||||
|
sle.valuation_rate,
|
||||||
sle.posting_date,
|
sle.posting_date,
|
||||||
sle.voucher_type,
|
sle.voucher_type,
|
||||||
sle.voucher_no,
|
sle.voucher_no,
|
||||||
@@ -506,7 +522,14 @@ class FIFOSlots:
|
|||||||
item_table = frappe.qb.DocType("Item")
|
item_table = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
item = frappe.qb.from_("Item").select(
|
item = frappe.qb.from_("Item").select(
|
||||||
"name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no"
|
"name",
|
||||||
|
"item_name",
|
||||||
|
"description",
|
||||||
|
"stock_uom",
|
||||||
|
"brand",
|
||||||
|
"item_group",
|
||||||
|
"has_serial_no",
|
||||||
|
"valuation_method",
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.filters.get("item_code"):
|
if self.filters.get("item_code"):
|
||||||
|
|||||||
@@ -1006,6 +1006,10 @@ class SerialBatchCreation:
|
|||||||
elif self.has_serial_no and not self.get("serial_nos"):
|
elif self.has_serial_no and not self.get("serial_nos"):
|
||||||
self.serial_nos = get_serial_nos_for_outward(kwargs)
|
self.serial_nos = get_serial_nos_for_outward(kwargs)
|
||||||
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
|
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
|
||||||
|
if self.get("posting_date"):
|
||||||
|
kwargs["posting_date"] = self.get("posting_date")
|
||||||
|
kwargs["posting_time"] = self.get("posting_time")
|
||||||
|
|
||||||
self.batches = get_available_batches(kwargs)
|
self.batches = get_available_batches(kwargs)
|
||||||
|
|
||||||
def set_auto_serial_batch_entries_for_inward(self):
|
def set_auto_serial_batch_entries_for_inward(self):
|
||||||
|
|||||||
@@ -562,12 +562,28 @@ class update_entries_after:
|
|||||||
self.new_items_found = False
|
self.new_items_found = False
|
||||||
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
|
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
|
||||||
self.affected_transactions: set[tuple[str, str]] = set()
|
self.affected_transactions: set[tuple[str, str]] = set()
|
||||||
self.reserved_stock = flt(self.args.reserved_stock)
|
self.reserved_stock = self.get_reserved_stock()
|
||||||
|
|
||||||
self.data = frappe._dict()
|
self.data = frappe._dict()
|
||||||
self.initialize_previous_data(self.args)
|
self.initialize_previous_data(self.args)
|
||||||
self.build()
|
self.build()
|
||||||
|
|
||||||
|
def get_reserved_stock(self):
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
posting_datetime = get_combine_datetime(self.args.posting_date, self.args.posting_time)
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sre)
|
||||||
|
.select(Sum(sre.reserved_qty) - Sum(sre.delivered_qty))
|
||||||
|
.where(
|
||||||
|
(sre.item_code == self.item_code)
|
||||||
|
& (sre.warehouse == self.args.warehouse)
|
||||||
|
& (sre.docstatus == 1)
|
||||||
|
& (sre.creation <= posting_datetime)
|
||||||
|
)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
return flt(query[0][0]) if query else 0.0
|
||||||
|
|
||||||
def set_precision(self):
|
def set_precision(self):
|
||||||
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
|
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||||
self.currency_precision = get_field_precision(
|
self.currency_precision = get_field_precision(
|
||||||
@@ -884,6 +900,10 @@ class update_entries_after:
|
|||||||
|
|
||||||
if not sle.is_adjustment_entry:
|
if not sle.is_adjustment_entry:
|
||||||
sle.stock_value_difference = stock_value_difference
|
sle.stock_value_difference = stock_value_difference
|
||||||
|
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
|
||||||
|
sle.stock_value_difference = get_stock_value_difference(
|
||||||
|
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||||
|
)
|
||||||
|
|
||||||
sle.doctype = "Stock Ledger Entry"
|
sle.doctype = "Stock Ledger Entry"
|
||||||
frappe.get_doc(sle).db_update()
|
frappe.get_doc(sle).db_update()
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
{{ _("Powered by {0}").format('<a href="https://frappe.io/erpnext?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import escape_html
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"):
|
|||||||
|
|
||||||
website_send_message(sender, message, subject)
|
website_send_message(sender, message, subject)
|
||||||
|
|
||||||
|
message = escape_html(message)
|
||||||
|
|
||||||
lead = customer = None
|
lead = customer = None
|
||||||
customer = frappe.db.sql(
|
customer = frappe.db.sql(
|
||||||
"""select distinct dl.link_name from `tabDynamic Link` dl
|
"""select distinct dl.link_name from `tabDynamic Link` dl
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/frappe/erpnext.git"
|
"url": "git+https://github.com/frappe/erpnext.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://erpnext.com",
|
"homepage": "https://frappe.io/erpnext",
|
||||||
"author": "Frappe Technologies Pvt. Ltd.",
|
"author": "Frappe Technologies Pvt. Ltd.",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|||||||
@@ -77,6 +77,6 @@ docstring-code-format = true
|
|||||||
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://erpnext.com/"
|
Homepage = "https://frappe.io/erpnext"
|
||||||
Repository = "https://github.com/frappe/erpnext.git"
|
Repository = "https://github.com/frappe/erpnext.git"
|
||||||
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
||||||
|
|||||||
Reference in New Issue
Block a user