Compare commits

...

97 Commits

Author SHA1 Message Date
Ankush Menat
41f44ba1c6 perf: cache journal entry balances for 5 minutes
If user is repeatedly adding multiple JV lines then everytime the value
will be refetched from database, reading GLE table can be very expensive
on large DBs.

AFAIK this function is only for UX and balances are anyways overidden
when saving the document.

Proper fix: maybe use closing balance here?
2024-03-02 15:53:15 +05:30
ruthra kumar
014d21a050 Merge pull request #40233 from frappe/mergify/bp/version-15-hotfix/pr-40149
fix: incorrect exchange rate if JE has multi parties (backport #40149)
2024-03-02 14:35:25 +05:30
ruthra kumar
05e4d1c240 test: exchange rate fetch on JE with multiple forex parties
(cherry picked from commit ed95d41a51)
2024-03-02 08:22:47 +00:00
ruthra kumar
4513d83f22 fix: don't override reference exchange rate
(cherry picked from commit eaac02655b)
2024-03-02 08:22:47 +00:00
ruthra kumar
b785901efa fix: incorrect exchange rate if JE has multi parties
(cherry picked from commit 694c17487d)
2024-03-02 08:22:45 +00:00
mergify[bot]
b398cc6579 fix: uom wise price in sales or purchase transaction (backport #40216) (#40225)
fix: uom wise price in sales or purchase transaction (#40216)

fix: uom wise price
(cherry picked from commit 13b05aa7fb)

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2024-03-02 13:00:52 +05:30
mergify[bot]
85ae9eee25 fix: serial no valuation rate (backport #40221) (#40223)
fix: serial no valuation rate (#40221)

(cherry picked from commit a5232d9c10)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-03-01 22:48:15 +05:30
ruthra kumar
70ab25a611 Merge pull request #40201 from frappe/mergify/bp/version-15-hotfix/pr-40197
fix: ignore self on GL account validation for Bank Account (backport #40197)
2024-03-01 09:45:39 +05:30
Raffael Meyer
0d8358bdd1 Merge pull request #40210 from frappe/mergify/bp/version-15-hotfix/pr-40203
fix(Project): filter department by company (backport #40203)
2024-02-29 23:34:38 +01:00
barredterra
13e334398e fix(Project): filter department by company
(cherry picked from commit 5e736f0d06)
2024-02-29 17:30:05 +00:00
ruthra kumar
1947a67f3d fix: ignore self on GL account validation for Bank Account
(cherry picked from commit 3c19186654)
2024-02-29 14:25:48 +00:00
ruthra kumar
8720115f01 Merge pull request #40199 from frappe/mergify/bp/version-15-hotfix/pr-40196
fix: include debit_to or credit_to account while fetching advance on Sales/Purchase Invoice (backport #40196)
2024-02-29 19:54:45 +05:30
ruthra kumar
a5b3c1e7cb test: advance pulling logic on Sales/Purchase Invoice
(cherry picked from commit 646e9ca0dd)
2024-02-29 12:22:48 +00:00
ruthra kumar
23d7a1fc76 fix: include Debit To/Credit To account while fetching advance
(cherry picked from commit 3327599c9d)
2024-02-29 12:22:48 +00:00
Gursheen Kaur Anand
c7ca929ddb Merge pull request #40189 from frappe/mergify/bp/version-15-hotfix/pr-40176
fix(minor): make warning for previously existing SO an alert (backport #40176)
2024-02-29 11:59:36 +05:30
Gursheen Anand
4f9b194fe9 fix: make warning for previously existing SO an alert
(cherry picked from commit 24dcd64c16)
2024-02-29 06:04:16 +00:00
Gursheen Kaur Anand
cda1c5ca10 Merge pull request #40182 from frappe/mergify/bp/version-15-hotfix/pr-40174
fix: promotional scheme remove free item if pricing rule matches (backport #40174)
2024-02-29 11:34:00 +05:30
mergify[bot]
9cb8d33923 fix(setup): avoid duplicate entry for Analytics role (backport #40183) (#40185)
fix(setup): avoid duplicate entry for Analytics role (#40183)

(cherry picked from commit 29f91a7919)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2024-02-29 11:09:24 +05:30
Gursheen Anand
6352bfe34e fix: remove free item row only if pricing rule matches
(cherry picked from commit fea20db262)
2024-02-29 04:49:35 +00:00
Deepesh Garg
930df21a5f Merge pull request #40170 from frappe/mergify/bp/version-15-hotfix/pr-40137
fix: default taxable value for item not found in item list (#40137)
2024-02-28 08:24:15 +05:30
mergify[bot]
6a63a6c98a fix: Supplier users not able to see RFQ on the Portal (backport #40161) (#40165)
* fix: Supplier users not able to see RFQ on the Portal (#40161)

(cherry picked from commit f8ba560394)

* chore: fix travis

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-27 22:30:26 +05:30
ljain112
7e43f6b7e0 fix: default taxable value for item not found in item list
(cherry picked from commit 5885978fc2)
2024-02-27 16:40:19 +00:00
Deepesh Garg
84a359c760 Merge pull request #40163 from vorasmit/ic-deps-v15
fix: add flags for repost to ensure correct accounting from India Compliance App (#40162)
2024-02-27 22:08:45 +05:30
Smit Vora
7d14ecfcb8 fix: add flags for repost to ensure correct accounting from India Compliance App 2024-02-27 20:30:28 +05:30
Gursheen Kaur Anand
07fd93a6f6 Merge pull request #40152 from frappe/mergify/bp/version-15-hotfix/pr-40095
fix: unique GL account for plaid bank accounts (backport #40095)
2024-02-27 17:08:37 +05:30
mergify[bot]
4784117a8f fix: parent warehouse checks in the production plan for sub-assemblies (backport #40150) (#40157)
fix: parent warehouse checks in the production plan for sub-assemblies (#40150)

(cherry picked from commit 6f5815e44f)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-27 17:03:21 +05:30
Gursheen Kaur Anand
7a9beb4d47 Merge pull request #40154 from frappe/mergify/bp/version-15-hotfix/pr-40102
feat: toggle updation of billed amount in previous purchase docs (backport #40102)
2024-02-27 16:40:14 +05:30
Gursheen Kaur Anand
87f36ce425 chore: fix typo 2024-02-27 16:24:14 +05:30
Gursheen Kaur Anand
48992cd205 chore: resolve conflicts 2024-02-27 16:18:27 +05:30
Gursheen Kaur Anand
99d1f1d67b chore: resolve conflicts 2024-02-27 16:15:49 +05:30
Gursheen Kaur Anand
eadea0207c chore: resolve conflicts 2024-02-27 16:13:49 +05:30
Gursheen Anand
e67d579ac4 test: pr billed amount against debit note
(cherry picked from commit 6d40844894)
2024-02-27 10:24:01 +00:00
Gursheen Anand
bac2f66344 test: po billed amount against debit note
(cherry picked from commit 81dbfe189e)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/test_purchase_order.py
2024-02-27 10:24:01 +00:00
Gursheen Anand
e7e8149fbe feat: update billed amount in PO and PR
(cherry picked from commit 9f6535472d)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
2024-02-27 10:24:00 +00:00
Gursheen Anand
36b442a951 fix: remove config for default bank account in test
(cherry picked from commit c42444ab3b)
2024-02-27 10:10:20 +00:00
Gursheen Anand
65853da505 fix: unique gl account for plaid bank accounts
(cherry picked from commit bf6e32a960)
2024-02-27 10:10:20 +00:00
Gursheen Kaur Anand
df7afc3154 Merge pull request #40145 from frappe/mergify/bp/version-15-hotfix/pr-40143
fix(minor): type error for missing form object in hide_company util (backport #40143)
2024-02-27 13:33:13 +05:30
Gursheen Anand
341f9030f5 fix: use frm instead of cur_frm
(cherry picked from commit ceeb8fc9e5)
2024-02-27 07:45:36 +00:00
Gursheen Anand
6b5e1cfeb7 fix: type error for missing frm obj
(cherry picked from commit 20fa3da950)
2024-02-27 07:45:36 +00:00
ruthra kumar
9f8a27833a Merge pull request #40135 from frappe/mergify/bp/version-15-hotfix/pr-40133
fix: reset advance amount on unreconciliation of Sales/Purchase Orders (backport #40133)
2024-02-27 10:33:07 +05:30
mergify[bot]
1860399ccb fix: use serial batch fields for packed items (backport #40140) (#40142)
fix: use serial batch fields for packed items (#40140)

(cherry picked from commit bc9c480246)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-27 07:57:37 +05:30
mergify[bot]
57bb031602 fix: currency symbol in landed cost voucher and material request (backport #40138) (#40141)
fix: currency symbol in landed cost voucher and material request (#40138)

(cherry picked from commit 8aa2b7c183)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-26 23:16:49 +05:30
mergify[bot]
9d19ec43c8 fix: Data too long for column 'serial_no' at row 1 (backport #40098) (#40139)
* fix: Data too long for column 'serial_no' at row 1 (#40098)

(cherry picked from commit 08caa7cfa1)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-26 23:00:01 +05:30
ruthra kumar
0694fd19fd fix: use correct variable name on hotfix branches 2024-02-26 20:35:22 +05:30
ruthra kumar
94bb6241ef test: advance paid update on sales/purchase order unreconciliation
(cherry picked from commit 1f01ff3487)
2024-02-26 12:22:55 +00:00
ruthra kumar
4d1f56c4bd fix: on unreconciliation, update advance paid
(cherry picked from commit c9e2f03a3a)
2024-02-26 12:22:55 +00:00
ruthra kumar
ad53633c66 Merge pull request #40118 from frappe/mergify/bp/version-15-hotfix/pr-40113
refactor: patch to setup dimensions in Reconciliation tool (backport #40113)
2024-02-26 14:21:40 +05:30
ruthra kumar
2bf6125b71 refactor: patch to setup dimensions in Reconciliation tool
(cherry picked from commit 461fb183fc)
2024-02-26 13:08:47 +05:30
Gursheen Kaur Anand
e68d2d1c7a Merge pull request #40109 from frappe/mergify/bp/version-15-hotfix/pr-40105
fix(minor): tax amount label according to party type (backport #40105)
2024-02-26 11:10:08 +05:30
Gursheen Kaur Anand
93f64396ac Merge pull request #40111 from frappe/mergify/bp/version-15-hotfix/pr-40062
fix: sales person / partner achieved targets in report (backport #40062)
2024-02-26 11:08:15 +05:30
Gursheen Anand
91c21d13e5 test: sales person target variance
(cherry picked from commit 7566c1ee78)
2024-02-26 05:09:45 +00:00
Gursheen Anand
38abfdb8ae feat: show contributed qty in transaction summary
(cherry picked from commit a823f16dff)
2024-02-26 05:09:45 +00:00
Gursheen Anand
194f46be57 fix: only consider contributed qty towards achieved targets
(cherry picked from commit 339698d172)
2024-02-26 05:09:45 +00:00
Gursheen Anand
d541ba3e61 fix: amount label according to party type
(cherry picked from commit 9c8d103d8a)
2024-02-26 05:05:27 +00:00
mergify[bot]
27703b54bd fix: capacity planning issue in the job card (backport #40092) (#40101)
fix: capacity planning issue in the job card (#40092)

* fix: capacity planning issue in the job card

* test: test case to test capacity planning for workstation

(cherry picked from commit 75f8464724)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-25 19:22:51 +05:30
ruthra kumar
9ffbf5623a Merge pull request #40097 from frappe/mergify/bp/version-15-hotfix/pr-40096
fix: Fiscal Year exception on demo data setup (backport #40096)
2024-02-25 16:09:51 +05:30
ruthra kumar
56ee843233 fix: Fiscal Year exception on demo data setup
(cherry picked from commit 3c3c57c674)
2024-02-25 10:13:09 +00:00
ruthra kumar
383dfdd4ab Merge pull request #40091 from frappe/mergify/bp/version-15-hotfix/pr-40073
refactor: update payments section on item removal (backport #40073)
2024-02-25 09:34:42 +05:30
ruthra kumar
5c269a9947 refactor: update payments section on item removal
(cherry picked from commit 406793a6ff)
2024-02-25 03:58:19 +00:00
mergify[bot]
cf5fa210bb fix: do not make MR against raw materials of available sub assemblies (backport #40085) (#40087)
fix: do not make MR against raw materials of available sub assemblies (#40085)

(cherry picked from commit 4c9048fb39)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-24 21:02:17 +05:30
mergify[bot]
53d943ec70 fix: Cannot read properties of undefined (backport #40081) (#40083)
fix: Cannot read properties of undefined

(cherry picked from commit 44ed52c5cf)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2024-02-24 17:04:36 +05:30
rohitwaghchaure
d755b9d38c Merge pull request #40080 from frappe/mergify/bp/version-15-hotfix/pr-40079
chore: change label name (backport #40079)
2024-02-24 13:29:04 +05:30
Rohit Waghchaure
824df72eeb fix: change label name
(cherry picked from commit 635174f1ce)
2024-02-24 07:40:56 +00:00
Gursheen Kaur Anand
e4baacd79e Merge pull request #40066 from frappe/mergify/bp/version-15-hotfix/pr-40040
fix: skip max discount validation for rate adjustment (backport #40040)
2024-02-23 15:10:38 +05:30
Gursheen Kaur Anand
38b384dfa2 Merge pull request #40064 from frappe/mergify/bp/version-15-hotfix/pr-40035
fix: delete PLE containing invoice in against (backport #40035)
2024-02-23 15:07:48 +05:30
ruthra kumar
10fe201bc8 Merge pull request #40068 from frappe/mergify/bp/version-15-hotfix/pr-39830
fix: check_credit_limit on_update_after_submit of Sales Order (backport #39830)
2024-02-23 14:48:08 +05:30
ruthra kumar
bcb280c5e8 test: credit limit on update after submit
(cherry picked from commit 467c0898e9)
2024-02-23 08:51:39 +00:00
Nihantra C. Patel
83d7111649 fix: check_credit_limit on_update_after_submit of Sales Order
(cherry picked from commit 17452b7693)
2024-02-23 08:51:38 +00:00
Gursheen Anand
cd42089e20 fix: skip SO & DN validation for debit note
(cherry picked from commit e2d16955dd)
2024-02-23 07:28:38 +00:00
Gursheen Anand
3b96aaeead fix: skip max discount validation for rate adjustment
(cherry picked from commit 5a3b133d65)
2024-02-23 07:28:37 +00:00
Ankush Menat
83b4cc5091 build: specify frappe dependency (#40061)
build: specify frappe dependency 

Added 15.10 as dependency because stock balance depends on https://github.com/frappe/frappe/pull/24346
2024-02-23 12:43:01 +05:30
Gursheen Anand
a75a69a01e fix: only check for delinked PLEs
(cherry picked from commit 146c5b3e16)
2024-02-23 07:10:08 +00:00
Gursheen Anand
190bd45bd7 fix: delete PLE containing invoice in against
(cherry picked from commit c1e1fd8829)
2024-02-23 07:10:08 +00:00
ruthra kumar
c752bec2d9 Merge pull request #40058 from frappe/mergify/bp/version-15-hotfix/pr-40011
fix: Ledger entries for Cr/Dr notes with POS Payments (backport #40011)
2024-02-23 06:20:30 +05:30
ruthra kumar
8fef484f0f Merge pull request #40056 from frappe/mergify/bp/version-15-hotfix/pr-39831
fix: remove cancelled payment entry from Payment Period Based On Invoice Date (backport #39831)
2024-02-23 06:20:14 +05:30
ruthra kumar
b40baf5e63 refactor: skip popup for POS invoices
(cherry picked from commit 3634c4c284)
2024-02-23 00:33:08 +00:00
ruthra kumar
2c8ba892ac test: ledger entries of Cr Note of POS Invoice
(cherry picked from commit 4288713abe)
2024-02-23 00:33:07 +00:00
ruthra kumar
2e07b03307 fix: Cr/Dr notes with POS Payments
(cherry picked from commit 68a23730f3)
2024-02-23 00:33:07 +00:00
Nihantra C. Patel
0be520331c fix: remove cancelled payment entry from PPBOID report
(cherry picked from commit 186cc3d748)
2024-02-23 00:30:50 +00:00
Nihantra C. Patel
72da3083e6 fix: remove cancelled payment entry from Payment Period Based On Invoice Date
(cherry picked from commit a2a8a8f2e0)
2024-02-23 00:30:50 +00:00
ruthra kumar
a3ddd326d6 Merge pull request #40049 from frappe/mergify/bp/version-15-hotfix/pr-39828
fix: translate Sales Pipeline Analytics report (backport #39828)
2024-02-22 20:46:55 +05:30
Nihantra C. Patel
1c5a7e29f2 fix: translatable columns in Sales Pipeline Analytics report
(cherry picked from commit c5050c935b)
2024-02-22 14:54:53 +00:00
mergify[bot]
4269ef8c98 fix: communication_date in party dashboards (backport #40005) (#40021)
fix: accommodate for changed orderby statement

(cherry picked from commit 87df7ff717)

Co-authored-by: Gursheen Anand <gursheen@frappe.io>
2024-02-22 16:20:02 +05:30
mergify[bot]
a543bf47ef fix: timesheet per billed state edge case (backport #40010) (#40029)
fix: timesheet per billed state edge case (#40010)

If value is 100.0000x then it won't set status correctly but will set it the next time it's loaded from db.

(cherry picked from commit 38e88db2c9)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2024-02-22 12:29:53 +05:30
mergify[bot]
c964c45f4e fix: negative stock error while making stock reconciliation (backport #40016) (#40026)
* fix: negative stock error while making stock reconciliation (#40016)

fix: negative stock error while making stock reco
(cherry picked from commit da184d709b)

# Conflicts:
#	erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-22 11:38:49 +05:30
Nabin Hait
6f364df48c Merge pull request #39990 from frappe/mergify/bp/version-15-hotfix/pr-39983
fix: Issues regarding asset cancellation and deletion (backport #39983)
2024-02-22 11:07:01 +05:30
mergify[bot]
eaa3849df4 fix: remove microsecond from posting datetime (backport #40017) (#40022)
fix: remove microsecond from posting datetime (#40017)

(cherry picked from commit 0b04d04da3)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-22 09:51:07 +05:30
mergify[bot]
9f8f3db953 fix: incorrect item name in MR (backport #40018) (#40024)
fix: incorrect item name in MR (#40018)

(cherry picked from commit 864d7ae04c)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-02-22 09:50:45 +05:30
Gursheen Kaur Anand
43c218c673 Merge pull request #40019 from frappe/mergify/bp/version-15-hotfix/pr-39999
fix: TypeError for item pricing rules (backport #39999)
2024-02-22 09:10:17 +05:30
Gursheen Anand
32d9642379 fix: check for pricing rules on item
(cherry picked from commit ecd83b12ab)
2024-02-22 03:37:17 +00:00
mergify[bot]
b9181e85dc perf: new column Posting Datetime in SLE to optimize stock ledger related queries (backport #39800) (#40004)
* perf: new column posting datetime in SLE to optimize stock ledger related queries

(cherry picked from commit d80ca523a4)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* test: test cases to test clash timestamp entries

(cherry picked from commit f04676aaed)

* chore: remove microsecond from posting_datetime

(cherry picked from commit a73ba2c0d2)

* chore: fix conflicts

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2024-02-21 21:14:28 +05:30
rohitwaghchaure
66a05087b8 Merge pull request #40001 from frappe/mergify/bp/version-15-hotfix/pr-39998
fix: Completed Work Orders report not working (backport #39998)
2024-02-21 17:44:44 +05:30
Rohit Waghchaure
ca03e9cfd3 fix: Completed Work Orders report not working
(cherry picked from commit 11f4cb914a)
2024-02-21 11:21:04 +00:00
Nabin Hait
692867427c fix: resolved conflict 2024-02-21 11:38:21 +05:30
Nabin Hait
a0c0ab7709 fix: resolved conflict 2024-02-21 11:37:47 +05:30
Nabin Hait
ee2d108bef fix: removed unwanted patch
(cherry picked from commit 85471533e9)
2024-02-21 05:45:27 +00:00
Nabin Hait
8eb2f67910 fix: Issues regarding asset cancellation and deletion
(cherry picked from commit 17f85de6fb)

# Conflicts:
#	erpnext/assets/doctype/asset/depreciation.py
#	erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
2024-02-21 05:45:27 +00:00
100 changed files with 1668 additions and 609 deletions

View File

@@ -57,7 +57,9 @@ class BankAccount(Document):
def validate_account(self):
if self.account:
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, msgprint, scrub
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
from frappe.utils.caching import redis_cache
import erpnext
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@@ -1478,6 +1479,7 @@ def get_outstanding(args):
@frappe.whitelist()
@redis_cache(ttl=5 * 60, user=True)
def get_party_account_and_balance(company, party_type, party, cost_center=None):
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)

View File

@@ -149,7 +149,7 @@ frappe.ui.form.on('Payment Entry', {
},
refresh: function(frm) {
erpnext.hide_company();
erpnext.hide_company(frm);
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);

View File

@@ -389,7 +389,10 @@ class PaymentEntry(AccountsController):
)
def set_missing_ref_details(
self, force: bool = False, update_ref_details_only_for: list | None = None
self,
force: bool = False,
update_ref_details_only_for: list | None = None,
ref_exchange_rate: float | None = None,
) -> None:
for d in self.get("references"):
if d.allocated_amount:
@@ -401,6 +404,8 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
if ref_exchange_rate:
ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():
if d.exchange_gain_loss:

View File

@@ -631,7 +631,12 @@ class PaymentReconciliation(Document):
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
filters={
"parent": ("in", journals),
"account": ("in", [self.receivable_payable_account]),
"party_type": self.party_type,
"party": self.party,
},
fields=[
"parent as `name`",
"exchange_rate",

View File

@@ -56,6 +56,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
self.cash = "Cash - _PR"
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
@@ -486,6 +487,91 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_payment_against_foreign_currency_journal(self):
transaction_date = nowdate()
self.supplier = "_Test Supplier USD"
self.supplier2 = make_supplier("_Test Supplier2 USD", "USD")
amount = 100
exc_rate1 = 80
exc_rate2 = 83
je = frappe.new_doc("Journal Entry")
je.posting_date = transaction_date
je.company = self.company
je.user_remark = "test"
je.multi_currency = 1
je.set(
"accounts",
[
{
"account": self.creditors_usd,
"party_type": "Supplier",
"party": self.supplier,
"exchange_rate": exc_rate1,
"cost_center": self.cost_center,
"credit": amount * exc_rate1,
"credit_in_account_currency": amount,
},
{
"account": self.creditors_usd,
"party_type": "Supplier",
"party": self.supplier2,
"exchange_rate": exc_rate2,
"cost_center": self.cost_center,
"credit": amount * exc_rate2,
"credit_in_account_currency": amount,
},
{
"account": self.expense_account,
"cost_center": self.cost_center,
"debit": (amount * exc_rate1) + (amount * exc_rate2),
"debit_in_account_currency": (amount * exc_rate1) + (amount * exc_rate2),
},
],
)
je.save().submit()
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
pe.payment_type = "Pay"
pe.party_type = "Supplier"
pe.party = self.supplier
pe.paid_to = self.creditors_usd
pe.paid_from = self.cash
pe.paid_amount = 8000
pe.received_amount = 100
pe.target_exchange_rate = exc_rate1
pe.paid_to_account_currency = "USD"
pe.save().submit()
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.receivable_payable_account = self.creditors_usd
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
pr.from_invoice_date = pr.to_invoice_date = transaction_date
pr.from_payment_date = pr.to_payment_date = transaction_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# There should no difference_amount as the Journal and Payment have same exchange rate - 'exc_rate1'
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
journals = frappe.db.get_all(
"Journal Entry Account",
filters={"reference_type": je.doctype, "reference_name": je.name, "docstatus": 1},
fields=["parent"],
)
self.assertEqual([], journals)
def test_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
@@ -1248,3 +1334,17 @@ def make_customer(customer_name, currency=None):
return customer.name
else:
return customer_name
def make_supplier(supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.type = "Individual"
if currency:
supplier.default_currency = currency
supplier.save()
return supplier.name
else:
return supplier_name

View File

@@ -654,7 +654,7 @@
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Text",
"hidden": 1,
"in_list_view": 1,
"label": "Serial No",
@@ -853,7 +853,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-02-04 16:36:25.665743",
"modified": "2024-02-25 15:50:17.140269",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -72,7 +72,7 @@ class POSInvoiceItem(Document):
rate_with_margin: DF.Currency
sales_order: DF.Link | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
service_end_date: DF.Date | None
service_start_date: DF.Date | None
service_stop_date: DF.Date | None

View File

@@ -22,6 +22,8 @@
"is_paid",
"is_return",
"return_against",
"update_billed_amount_in_purchase_order",
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"tax_withholding_category",
"amended_from",
@@ -412,6 +414,20 @@
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_purchase_order",
"fieldtype": "Check",
"label": "Update Billed Amount in Purchase Order"
},
{
"default": "1",
"depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_purchase_receipt",
"fieldtype": "Check",
"label": "Update Billed Amount in Purchase Receipt"
},
{
"fieldname": "section_addresses",
"fieldtype": "Section Break",
@@ -1612,7 +1628,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-11-29 15:35:44.697496",
"modified": "2024-02-25 11:20:28.366808",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1675,4 +1691,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -214,6 +214,8 @@ class PurchaseInvoice(BuyingController):
total_qty: DF.Float
total_taxes_and_charges: DF.Currency
unrealized_profit_loss_account: DF.Link | None
update_billed_amount_in_purchase_order: DF.Check
update_billed_amount_in_purchase_receipt: DF.Check
update_stock: DF.Check
use_company_roundoff_cost_center: DF.Check
use_transaction_date_exchange_rate: DF.Check
@@ -679,6 +681,11 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).on_submit()
self.check_prev_docstatus()
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return
self.status_updater = []
self.update_status_updater_args()
self.update_prevdoc_status()
@@ -1426,6 +1433,10 @@ class PurchaseInvoice(BuyingController):
self.check_on_hold_or_closed_status()
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return
self.status_updater = []
self.update_status_updater_args()
self.update_prevdoc_status()
@@ -1520,6 +1531,9 @@ class PurchaseInvoice(BuyingController):
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
def update_billing_status_in_pr(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
return
updated_pr = []
po_details = []

View File

@@ -88,6 +88,7 @@ class RepostAccountingLedger(Document):
).append(gle.update({"old": True}))
def generate_preview_data(self):
frappe.flags.through_repost_accounting_ledger = True
self.gl_entries = []
self.get_existing_ledger_entries()
for x in self.vouchers:
@@ -141,6 +142,7 @@ class RepostAccountingLedger(Document):
@frappe.whitelist()
def start_repost(account_repost_doc=str) -> None:
frappe.flags.through_repost_accounting_ledger = True
if account_repost_doc:
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)

View File

@@ -269,7 +269,7 @@ class SalesInvoice(SellingController):
super(SalesInvoice, self).validate()
self.validate_auto_set_posting_time()
if not self.is_pos:
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()
self.set_tax_withholding()
@@ -446,7 +446,11 @@ class SalesInvoice(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
self.make_bundle_using_old_serial_batch_fields()
for table_name in ["items", "packed_items"]:
if not self.get(table_name):
continue
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_ledger()
# this sequence because outstanding may get -ve
@@ -1472,9 +1476,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
else payment_mode.amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
"against_voucher": self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
},

View File

@@ -1088,6 +1088,44 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, 10)
def test_ledger_entries_of_return_pos_invoice(self):
make_pos_profile()
pos = create_sales_invoice(do_not_save=True)
pos.is_pos = 1
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos.save().submit()
self.assertEqual(pos.outstanding_amount, 0.0)
self.assertEqual(pos.status, "Paid")
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
pos_return = make_sales_return(pos.name)
pos_return.save().submit()
pos_return.reload()
pos.reload()
self.assertEqual(pos_return.is_return, 1)
self.assertEqual(pos_return.return_against, pos.name)
self.assertEqual(pos_return.outstanding_amount, 0.0)
self.assertEqual(pos_return.status, "Return")
self.assertEqual(pos.outstanding_amount, 0.0)
self.assertEqual(pos.status, "Credit Note Issued")
expected = (
("Cash - _TC", 0.0, 100.0, pos_return.name, None),
("Debtors - _TC", 0.0, 100.0, pos_return.name, pos_return.name),
("Debtors - _TC", 100.0, 0.0, pos_return.name, pos_return.name),
("Sales - _TC", 100.0, 0.0, pos_return.name, None),
)
res = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pos_return.name, "is_cancelled": 0},
fields=["account", "debit", "credit", "voucher_no", "against_voucher"],
order_by="account, debit, credit",
as_list=1,
)
self.assertEqual(expected, res)
def test_pos_with_no_gl_entry_for_change_amount(self):
frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 0)
@@ -3564,6 +3602,33 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
set_advance_flag(company="_Test Company", flag=0, default_account="")
def test_pulling_advance_based_on_debit_to(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
account_name="Debtors 2",
company="_Test Company",
account_type="Receivable",
)
si = create_sales_invoice(do_not_submit=True)
si.debit_to = debtors2
si.save()
pe = create_payment_entry(
company=si.company,
payment_type="Receive",
party_type="Customer",
party=si.customer,
paid_from=debtors2,
paid_to="Cash - _TC",
paid_amount=1000,
)
pe.submit()
advances = si.get_advance_entries()
self.assertEqual(1, len(advances))
self.assertEqual(advances[0].reference_name, pe.name)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -625,7 +625,7 @@
{
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Text",
"label": "Serial No",
"oldfieldname": "serial_no",
"oldfieldtype": "Small Text"
@@ -926,7 +926,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-02-04 11:52:16.106541",
"modified": "2024-02-25 15:56:44.828634",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -75,7 +75,7 @@ class SalesInvoiceItem(Document):
sales_invoice_item: DF.Data | None
sales_order: DF.Link | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
service_end_date: DF.Date | None
service_start_date: DF.Date | None
service_stop_date: DF.Date | None

View File

@@ -8,6 +8,7 @@ from frappe.utils import today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
@@ -49,6 +50,16 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
)
return pe
def create_sales_order(self):
so = make_sales_order(
company=self.company,
customer=self.customer,
item=self.item,
rate=100,
transaction_date=today(),
)
return so
def test_01_unreconcile_invoice(self):
si1 = self.create_sales_invoice()
si2 = self.create_sales_invoice()
@@ -314,3 +325,41 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
),
1,
)
def test_05_unreconcile_order(self):
so = self.create_sales_order()
pe = self.create_payment_entry()
# Allocation payment against Sales Order
pe.paid_amount = 100
pe.append(
"references",
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 100},
)
pe.save().submit()
# Assert 'Advance Paid'
so.reload()
self.assertEqual(so.advance_paid, 100)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 1)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([so.name], allocations)
# unreconcile so
unreconcile.save().submit()
# Assert 'Advance Paid'
so.reload()
pe.reload()
self.assertEqual(so.advance_paid, 0)
self.assertEqual(len(pe.references), 0)
self.assertEqual(pe.unallocated_amount, 100)

View File

@@ -82,6 +82,9 @@ class UnreconcilePayment(Document):
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)

View File

@@ -9,7 +9,7 @@ from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Date, Sum
from frappe.query_builder.functions import Abs, Count, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -784,34 +784,37 @@ def get_timeline_data(doctype, name):
from frappe.desk.form.load import get_communication_data
out = {}
fields = "creation, count(*)"
after = add_years(None, -1).strftime("%Y-%m-%d")
group_by = "group by Date(creation)"
data = get_communication_data(
doctype,
name,
after=after,
group_by="group by creation",
fields="C.creation as creation, count(C.name)",
group_by="group by communication_date",
fields="C.communication_date as communication_date, count(C.name)",
as_dict=False,
)
# fetch and append data from Activity Log
data += frappe.db.sql(
"""select {fields}
from `tabActivity Log`
where (reference_doctype=%(doctype)s and reference_name=%(name)s)
or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s)
or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s)
and status!='Success' and creation > {after}
{group_by} order by creation desc
""".format(
fields=fields, group_by=group_by, after=after
),
{"doctype": doctype, "name": name},
as_dict=False,
)
activity_log = frappe.qb.DocType("Activity Log")
data += (
frappe.qb.from_(activity_log)
.select(activity_log.communication_date, Count(activity_log.name))
.where(
(
((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name))
| ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name))
| (
(activity_log.reference_doctype.isin(["Quotation", "Opportunity"]))
& (activity_log.timeline_name == name)
)
)
& (activity_log.status != "Success")
& (activity_log.creation > after)
)
.groupby(activity_log.communication_date)
.orderby(activity_log.communication_date, order=frappe.qb.desc)
).run()
timeline_items = dict(data)

View File

@@ -975,7 +975,7 @@ class GrossProfitGenerator(object):
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc)
.run(as_dict=True)
)

View File

@@ -163,7 +163,7 @@ def get_entries(filters):
"""select
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
from `tabGL Entry`
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') {0}
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {0}
""".format(
get_conditions(filters)
),

View File

@@ -242,7 +242,7 @@ def get_columns(filters):
"width": 120,
},
{
"label": _("Tax Amount"),
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,

View File

@@ -714,7 +714,7 @@ def update_reference_in_payment_entry(
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details()
payment_entry.set_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal(
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
@@ -970,46 +970,6 @@ def get_currency_precision():
return precision
def get_stock_rbnb_difference(posting_date, company):
stock_items = frappe.db.sql_list(
"""select distinct item_code
from `tabStock Ledger Entry` where company=%s""",
company,
)
pr_valuation_amount = frappe.db.sql(
"""
select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor)
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s
and pr.posting_date <= %s and pr_item.item_code in (%s)"""
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
tuple([company, posting_date] + stock_items),
)[0][0]
pi_valuation_amount = frappe.db.sql(
"""
select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor)
from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi
where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s
and pi.posting_date <= %s and pi_item.item_code in (%s)"""
% ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
tuple([company, posting_date] + stock_items),
)[0][0]
# Balance should be
stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2)
# Balance as per system
stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value(
"Company", company, "abbr"
)
sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False)
# Amount should be credited
return flt(stock_rbnb) + flt(sys_bal)
def get_held_invoices(party_type, party):
"""
Returns a list of names Purchase Invoices for the given party that are on hold
@@ -1412,8 +1372,7 @@ def sort_stock_vouchers_by_posting_date(
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_date)
.orderby(sle.posting_time)
.orderby(sle.posting_datetime)
.orderby(sle.creation)
).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]

View File

@@ -561,13 +561,14 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
def reverse_depreciation_entry_made_after_disposal(asset, date):
for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"):
continue
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date:
if schedule.schedule_date == date and schedule.journal_entry:
if not disposal_was_made_on_original_schedule_date(
schedule_idx, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()

View File

@@ -6,7 +6,7 @@ frappe.provide("erpnext.assets");
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
setup() {
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle', 'Asset Movement'];
this.setup_posting_date_time_check();
}

View File

@@ -138,6 +138,7 @@ class AssetCapitalization(StockController):
"Repost Item Valuation",
"Serial and Batch Bundle",
"Asset",
"Asset Movement",
)
self.cancel_target_asset()
self.update_stock_ledger()
@@ -147,6 +148,7 @@ class AssetCapitalization(StockController):
def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset:
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.db_set("capitalized_in", None)
if asset_doc.docstatus == 1:
asset_doc.cancel()

View File

@@ -107,7 +107,7 @@
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Text",
"hidden": 1,
"label": "Serial No",
"print_hide": 1
@@ -178,7 +178,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-04 16:41:09.239762",
"modified": "2024-02-25 15:57:35.007501",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",

View File

@@ -24,7 +24,7 @@ class AssetCapitalizationStockItem(Document):
parentfield: DF.Data
parenttype: DF.Data
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
stock_qty: DF.Float
stock_uom: DF.Link
use_serial_batch_fields: DF.Check

View File

@@ -418,14 +418,13 @@ class AssetDepreciationSchedule(Document):
)
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
if (
n == cint(final_number_of_depreciations) - 1
and flt(value_after_depreciation) != flt(row.expected_value_after_useful_life)
) or flt(value_after_depreciation) < flt(row.expected_value_after_useful_life):
depreciation_amount += flt(value_after_depreciation) - flt(
row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
):
depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
skip_row = True
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
@@ -813,15 +812,11 @@ def make_draft_asset_depr_schedules_if_not_present(asset_doc):
asset_depr_schedules_names = []
for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Draft", row.finance_book
asset_depr_schedule = get_asset_depr_schedule_name(
asset_doc.name, ["Draft", "Active"], row.finance_book
)
active_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Active", row.finance_book
)
if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name:
if not asset_depr_schedule:
name = make_draft_asset_depr_schedule(asset_doc, row)
asset_depr_schedules_names.append(name)
@@ -997,16 +992,20 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
finance_book_filter = ["finance_book", "is", "not set"]
if finance_book:
if finance_book is None:
finance_book_filter = ["finance_book", "is", "not set"]
else:
finance_book_filter = ["finance_book", "=", finance_book]
if isinstance(status, str):
status = [status]
return frappe.db.get_value(
doctype="Asset Depreciation Schedule",
filters=[
["asset", "=", asset_name],
finance_book_filter,
["status", "=", status],
["status", "in", status],
],
)

View File

@@ -1037,6 +1037,38 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name}))
def test_po_billed_amount_against_return_entry(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
# Create a Purchase Order and Fully Bill it
po = create_purchase_order()
pi = make_pi_from_po(po.name)
pi.insert()
pi.submit()
# Debit Note - 50% Qty & enable updating PO billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_order = 1
pi_return.submit()
# Check if the billed amount reduced
po.reload()
self.assertEqual(po.per_billed, 50)
pi_return.reload()
pi_return.cancel()
# Debit Note - 50% Qty & disable updating PO billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_order = 0
pi_return.submit()
# Check if the billed amount stayed the same
po.reload()
self.assertEqual(po.per_billed, 100)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -205,10 +205,30 @@ class RequestforQuotation(BuyingController):
contact.save(ignore_permissions=True)
if rfq_supplier.supplier:
self.update_user_in_supplier(rfq_supplier.supplier, user.name)
if not rfq_supplier.contact:
# return contact to later update, RFQ supplier row's contact
return contact.name
def update_user_in_supplier(self, supplier, user):
"""Update user in Supplier."""
if not frappe.db.exists("Portal User", {"parent": supplier, "user": user}):
supplier_doc = frappe.get_doc("Supplier", supplier)
supplier_doc.append(
"portal_users",
{
"user": user,
},
)
supplier_doc.flags.ignore_validate = True
supplier_doc.flags.ignore_mandatory = True
supplier_doc.flags.ignore_permissions = True
supplier_doc.save()
def create_user(self, rfq_supplier, link):
user = frappe.get_doc(
{
@@ -245,6 +265,10 @@ class RequestforQuotation(BuyingController):
"user_fullname": full_name,
}
)
if not self.email_template:
return
email_template = frappe.get_doc("Email Template", self.email_template)
message = frappe.render_template(email_template.response_, doc_args)
subject = frappe.render_template(email_template.subject, doc_args)

View File

@@ -138,6 +138,33 @@ class TestRequestforQuotation(FrappeTestCase):
get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
self.assertEqual(frappe.local.response.type, "pdf")
def test_portal_user_with_new_supplier(self):
supplier_doc = frappe.get_doc(
{
"doctype": "Supplier",
"supplier_name": "Test Supplier for RFQ",
"supplier_group": "_Test Supplier Group",
}
).insert()
self.assertFalse(supplier_doc.portal_users)
rfq = make_request_for_quotation(
supplier_data=[
{
"supplier": supplier_doc.name,
"supplier_name": supplier_doc.supplier_name,
"email_id": "123_testrfquser@example.com",
}
],
do_not_submit=True,
)
for rfq_supplier in rfq.suppliers:
rfq.update_supplier_contact(rfq_supplier, rfq.get_link())
supplier_doc.reload()
self.assertTrue(supplier_doc.portal_users[0].user)
def make_request_for_quotation(**args) -> "RequestforQuotation":
"""

View File

@@ -216,7 +216,8 @@ class AccountsController(TransactionBase):
)
)
if self.get("is_return") and self.get("return_against"):
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
# if self.get("is_return") and self.get("return_against"):
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
frappe.msgprint(
_(
@@ -345,6 +346,12 @@ class AccountsController(TransactionBase):
ple = frappe.qb.DocType("Payment Ledger Entry")
frappe.qb.from_(ple).delete().where(
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
| (
(ple.against_voucher_type == self.doctype)
& (ple.against_voucher_no == self.name)
& ple.delinked
== 1
)
).run()
frappe.db.sql(
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
@@ -1126,21 +1133,24 @@ class AccountsController(TransactionBase):
self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True):
party_account = []
if self.doctype == "Sales Invoice":
party_type = "Customer"
party = self.customer
amount_field = "credit_in_account_currency"
order_field = "sales_order"
order_doctype = "Sales Order"
party_account.append(self.debit_to)
else:
party_type = "Supplier"
party = self.supplier
amount_field = "debit_in_account_currency"
order_field = "purchase_order"
order_doctype = "Purchase Order"
party_account.append(self.credit_to)
party_account = get_party_account(
party_type, party=party, company=self.company, include_advance=True
party_account.extend(
get_party_account(party_type, party=party, company=self.company, include_advance=True)
)
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))

View File

@@ -824,7 +824,8 @@ class BuyingController(SubcontractingController):
if self.doctype == "Purchase Invoice" and not self.get("update_stock"):
return
frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name)
asset_movement = frappe.db.get_value("Asset Movement", {"reference_name": self.name}, "name")
frappe.delete_doc("Asset Movement", asset_movement, force=1)
def validate_schedule_date(self):
if not self.get("items"):

View File

@@ -28,7 +28,8 @@ class SellingController(StockController):
def validate(self):
super(SellingController, self).validate()
self.validate_items()
self.validate_max_discount()
if not self.get("is_debit_note"):
self.validate_max_discount()
self.validate_selling_price()
self.set_qty_as_per_stock_uom()
self.set_po_nos(for_validate=True)
@@ -706,6 +707,9 @@ def set_default_income_account_for_item(obj):
def get_serial_and_batch_bundle(child, parent):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if child.get("use_serial_batch_fields"):
return
if not frappe.db.get_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
):

View File

@@ -158,7 +158,7 @@ class StockController(AccountsController):
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
def make_bundle_using_old_serial_batch_fields(self):
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
@@ -169,7 +169,9 @@ class StockController(AccountsController):
if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
return
table_name = "items"
if not table_name:
table_name = "items"
if self.doctype == "Asset Capitalization":
table_name = "stock_items"
@@ -192,6 +194,12 @@ class StockController(AccountsController):
qty = row.qty
type_of_transaction = "Inward"
warehouse = row.warehouse
elif table_name == "packed_items":
qty = row.qty
warehouse = row.warehouse
type_of_transaction = "Outward"
if self.is_return:
type_of_transaction = "Inward"
else:
qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty
type_of_transaction = get_type_of_transaction(self, row)

View File

@@ -1020,7 +1020,7 @@ def get_itemised_tax_breakup_data(doc):
for item_code, taxes in itemised_tax.items():
itemised_tax_data.append(
frappe._dict(
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes}
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code, 0), **taxes}
)
)

View File

@@ -41,7 +41,9 @@ class SalesPipelineAnalytics(object):
month_list = self.get_month_list()
for month in month_list:
self.columns.append({"fieldname": month, "fieldtype": based_on, "label": month, "width": 200})
self.columns.append(
{"fieldname": month, "fieldtype": based_on, "label": _(month), "width": 200}
)
elif self.filters.get("range") == "Quarterly":
for quarter in range(1, 5):
@@ -156,7 +158,7 @@ class SalesPipelineAnalytics(object):
for column in self.columns:
if column["fieldname"] != "opportunity_owner" and column["fieldname"] != "sales_stage":
labels.append(column["fieldname"])
labels.append(_(column["fieldname"]))
self.chart = {"data": {"labels": labels, "datasets": datasets}, "type": "line"}

View File

@@ -10,7 +10,6 @@ from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, sbool, today
from plaid.errors import ItemError
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
@@ -90,9 +89,15 @@ def add_bank_accounts(response, bank, company):
bank = json.loads(bank)
result = []
default_gl_account = get_default_bank_cash_account(company, "Bank")
if not default_gl_account:
frappe.throw(_("Please setup a default bank account for company {0}").format(company))
parent_gl_account = frappe.db.get_all(
"Account", {"company": company, "account_type": "Bank", "is_group": 1, "disabled": 0}
)
if not parent_gl_account:
frappe.throw(
_(
"Please setup and enable a group account with the Account Type - {0} for the company {1}"
).format(frappe.bold("Bank"), company)
)
for account in response["accounts"]:
acc_type = frappe.db.get_value("Bank Account Type", account["type"])
@@ -108,11 +113,22 @@ def add_bank_accounts(response, bank, company):
if not existing_bank_account:
try:
gl_account = frappe.get_doc(
{
"doctype": "Account",
"account_name": account["name"] + " - " + response["institution"]["name"],
"parent_account": parent_gl_account[0].name,
"account_type": "Bank",
"company": company,
}
)
gl_account.insert(ignore_if_duplicate=True)
new_account = frappe.get_doc(
{
"doctype": "Bank Account",
"bank": bank["bank_name"],
"account": default_gl_account.account,
"account": gl_account.name,
"account_name": account["name"],
"account_type": account.get("type", ""),
"account_subtype": account.get("subtype", ""),

View File

@@ -7,7 +7,6 @@ import unittest
import frappe
from frappe.utils.response import json_handler
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype,
add_account_type,
@@ -72,14 +71,6 @@ class TestPlaidSettings(unittest.TestCase):
bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
company = frappe.db.get_single_value("Global Defaults", "default_company")
if frappe.db.get_value("Company", company, "default_bank_account") is None:
frappe.db.set_value(
"Company",
company,
"default_bank_account",
get_default_bank_cash_account(company, "Cash").get("account"),
)
add_bank_accounts(bank_accounts, bank, company)
transactions = {

View File

@@ -18,7 +18,7 @@ frappe.ui.form.on('Blanket Order', {
},
refresh: function(frm) {
erpnext.hide_company();
erpnext.hide_company(frm);
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({

View File

@@ -1071,8 +1071,7 @@ def get_valuation_rate(data):
frappe.qb.from_(sle)
.select(sle.valuation_rate)
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
.orderby(sle.posting_date, order=frappe.qb.desc)
.orderby(sle.posting_time, order=frappe.qb.desc)
.orderby(sle.posting_datetime, order=frappe.qb.desc)
.orderby(sle.creation, order=frappe.qb.desc)
.limit(1)
).run(as_dict=True)

View File

@@ -239,12 +239,12 @@ class JobCard(Document):
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
def get_overlap_for(self, args, check_next_available_slot=False):
def get_overlap_for(self, args):
time_logs = []
time_logs.extend(self.get_time_logs(args, "Job Card Time Log", check_next_available_slot))
time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", check_next_available_slot))
time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
if not time_logs:
return {}
@@ -269,7 +269,7 @@ class JobCard(Document):
self.workstation = workstation_time.get("workstation")
return workstation_time
return time_logs[-1]
return time_logs[0]
def has_overlap(self, production_capacity, time_logs):
overlap = False
@@ -308,7 +308,7 @@ class JobCard(Document):
return True
return overlap
def get_time_logs(self, args, doctype, check_next_available_slot=False):
def get_time_logs(self, args, doctype):
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType(doctype)
@@ -318,9 +318,6 @@ class JobCard(Document):
((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
]
if check_next_available_slot:
time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
query = (
frappe.qb.from_(jctl)
.from_(jc)
@@ -395,18 +392,28 @@ class JobCard(Document):
def validate_overlap_for_workstation(self, args, row):
# get the last record based on the to time from the job card
data = self.get_overlap_for(args, check_next_available_slot=True)
data = self.get_overlap_for(args)
if not self.workstation:
workstations = get_workstations(self.workstation_type)
if workstations:
# Get the first workstation
self.workstation = workstations[0]
if not data:
row.planned_start_time = args.from_time
return
if data:
if data.get("planned_start_time"):
row.planned_start_time = get_datetime(data.planned_start_time)
args.planned_start_time = get_datetime(data.planned_start_time)
else:
row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
args.from_time = args.planned_start_time
args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins)
self.validate_overlap_for_workstation(args, row)
def check_workstation_time(self, row):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)

View File

@@ -518,6 +518,12 @@ frappe.ui.form.on("Production Plan Sales Order", {
}
});
frappe.ui.form.on("Production Plan Sub Assembly Item", {
fg_warehouse(frm, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
},
})
frappe.tour['Production Plan'] = [
{
fieldname: "get_items_from",

View File

@@ -421,9 +421,11 @@
"fieldtype": "Column Break"
},
{
"description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",
"fieldname": "sub_assembly_warehouse",
"fieldtype": "Link",
"label": "Sub Assembly Warehouse",
"mandatory_depends_on": "eval:doc.skip_available_sub_assembly_item === 1",
"options": "Warehouse"
},
{
@@ -437,7 +439,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-02-11 15:42:47.642481",
"modified": "2024-02-27 13:34:20.692211",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -894,8 +894,8 @@ class ProductionPlan(Document):
sub_assembly_items_store = [] # temporary store to process all subassembly items
for row in self.po_items:
if self.skip_available_sub_assembly_item and not row.warehouse:
frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse:
frappe.throw(_("Row #{0}: Please select the Sub Assembly Warehouse").format(row.idx))
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
@@ -905,15 +905,24 @@ class ProductionPlan(Document):
bom_data = []
warehouse = (
(self.sub_assembly_warehouse or row.warehouse)
if self.skip_available_sub_assembly_item
else None
)
warehouse = (self.sub_assembly_warehouse) if self.skip_available_sub_assembly_item else None
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
sub_assembly_items_store.extend(bom_data)
if not sub_assembly_items_store and self.skip_available_sub_assembly_item:
message = (
_(
"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}."
).format(self.sub_assembly_warehouse)
+ "<br><br>"
)
message += _(
"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox."
)
frappe.msgprint(message, title=_("Note"))
if self.combine_sub_items:
# Combine subassembly items
sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
@@ -926,15 +935,19 @@ class ProductionPlan(Document):
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
"Modify bom_data, set additional details."
is_group_warehouse = frappe.db.get_value("Warehouse", self.sub_assembly_warehouse, "is_group")
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or (
"Subcontract" if data.is_sub_contracted_item else "In House"
)
if not is_group_warehouse:
data.fg_warehouse = self.sub_assembly_warehouse
def set_default_supplier_for_subcontracting_order(self):
items = [
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"
@@ -1478,7 +1491,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details = frappe._dict()
sub_assembly_items = {}
if doc.get("skip_available_sub_assembly_item"):
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
for d in doc.get("sub_assembly_items"):
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
@@ -1507,19 +1520,17 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
if bom_no:
if (
data.get("include_exploded_items")
and doc.get("sub_assembly_items")
and doc.get("skip_available_sub_assembly_item")
):
item_details = get_raw_materials_of_sub_assembly_items(
item_details,
company,
bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=planned_qty,
)
if data.get("include_exploded_items") and doc.get("skip_available_sub_assembly_item"):
item_details = {}
if doc.get("sub_assembly_items"):
item_details = get_raw_materials_of_sub_assembly_items(
item_details,
company,
bom_no,
include_non_stock_items,
sub_assembly_items,
planned_qty=planned_qty,
)
elif data.get("include_exploded_items") and include_subcontracted_items:
# fetch exploded items from BOM
@@ -1692,34 +1703,37 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
if warehouse:
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
if bin_dict and bin_dict[0].projected_qty > 0:
if bin_dict[0].projected_qty > stock_qty:
continue
else:
stock_qty = stock_qty - bin_dict[0].projected_qty
for _bin_dict in bin_details:
if _bin_dict.projected_qty > 0:
if _bin_dict.projected_qty > stock_qty:
stock_qty = 0
continue
else:
stock_qty = stock_qty - _bin_dict.projected_qty
bom_data.append(
frappe._dict(
{
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
}
if stock_qty > 0:
bom_data.append(
frappe._dict(
{
"parent_item_code": parent_item_code,
"description": d.description,
"production_item": d.item_code,
"item_name": d.item_name,
"stock_uom": d.stock_uom,
"uom": d.stock_uom,
"bom_no": d.value,
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
"stock_qty": stock_qty,
}
)
)
)
if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
def set_default_warehouses(row, default_warehouses):

View File

@@ -1205,6 +1205,7 @@ class TestProductionPlan(FrappeTestCase):
ignore_existing_ordered_qty=1,
do_not_submit=1,
skip_available_sub_assembly_item=1,
sub_assembly_warehouse="_Test Warehouse - _TC",
warehouse="_Test Warehouse - _TC",
)
@@ -1232,6 +1233,35 @@ class TestProductionPlan(FrappeTestCase):
if row.item_code == "SubAssembly2 For SUB Test":
self.assertEqual(row.quantity, 10)
def test_sub_assembly_and_their_raw_materials_exists(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree = {
"FG1 For SUB Test": {
"SAB1 For SUB Test": {"CP1 For SUB Test": {}},
"SAB2 For SUB Test": {},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
for item in ["SAB1 For SUB Test", "SAB2 For SUB Test"]:
make_stock_entry(item_code=item, qty=10, rate=100, target="_Test Warehouse - _TC")
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=10,
ignore_existing_ordered_qty=1,
do_not_submit=1,
skip_available_sub_assembly_item=1,
warehouse="_Test Warehouse - _TC",
)
items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
)
self.assertFalse(items)
def test_transfer_and_purchase_mrp_for_purchase_uom(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -1309,6 +1339,7 @@ class TestProductionPlan(FrappeTestCase):
ignore_existing_ordered_qty=1,
do_not_submit=1,
skip_available_sub_assembly_item=1,
sub_assembly_warehouse="_Test Warehouse - _TC",
warehouse="_Test Warehouse - _TC",
)
@@ -1561,6 +1592,48 @@ class TestProductionPlan(FrappeTestCase):
for row in work_orders:
self.assertEqual(row.qty, wo_qty[row.name])
def test_parent_warehouse_for_sub_assembly_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
parent_warehouse = "_Test Warehouse Group - _TC"
sub_warehouse = create_warehouse("Sub Warehouse", company="_Test Company")
fg_item = make_item(properties={"is_stock_item": 1}).name
sf_item = make_item(properties={"is_stock_item": 1}).name
rm_item = make_item(properties={"is_stock_item": 1}).name
bom_tree = {fg_item: {sf_item: {rm_item: {}}}}
create_nested_bom(bom_tree, prefix="")
pln = create_production_plan(
item_code=fg_item,
planned_qty=10,
warehouse="_Test Warehouse - _TC",
sub_assembly_warehouse=parent_warehouse,
skip_available_sub_assembly_item=1,
do_not_submit=1,
skip_getting_mr_items=1,
)
pln.get_sub_assembly_items()
for row in pln.sub_assembly_items:
self.assertFalse(row.fg_warehouse)
self.assertEqual(row.production_item, sf_item)
self.assertEqual(row.qty, 10.0)
make_stock_entry(item_code=sf_item, qty=5, target=sub_warehouse, rate=100)
pln.sub_assembly_items = []
pln.get_sub_assembly_items()
self.assertEqual(pln.sub_assembly_warehouse, parent_warehouse)
for row in pln.sub_assembly_items:
self.assertFalse(row.fg_warehouse)
self.assertEqual(row.production_item, sf_item)
self.assertEqual(row.qty, 5.0)
def create_production_plan(**args):
"""

View File

@@ -11,6 +11,7 @@
"bom_no",
"column_break_6",
"planned_qty",
"stock_uom",
"warehouse",
"planned_start_date",
"section_break_9",
@@ -18,7 +19,6 @@
"ordered_qty",
"column_break_17",
"description",
"stock_uom",
"produced_qty",
"reference_section",
"sales_order",
@@ -65,6 +65,7 @@
"width": "100px"
},
{
"columns": 1,
"fieldname": "planned_qty",
"fieldtype": "Float",
"in_list_view": 1,
@@ -80,6 +81,7 @@
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
@@ -141,8 +143,10 @@
"width": "200px"
},
{
"columns": 1,
"fieldname": "stock_uom",
"fieldtype": "Link",
"in_list_view": 1,
"label": "UOM",
"oldfieldname": "stock_uom",
"oldfieldtype": "Data",
@@ -216,7 +220,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-11-25 14:15:40.061514",
"modified": "2024-02-27 13:24:43.571844",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",

View File

@@ -101,7 +101,6 @@
"columns": 1,
"fieldname": "bom_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Level (BOM)",
"read_only": 1
},
@@ -149,8 +148,10 @@
"label": "Indent"
},
{
"columns": 2,
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Target Warehouse",
"options": "Warehouse"
},
@@ -170,6 +171,7 @@
"options": "Supplier"
},
{
"columns": 1,
"fieldname": "schedule_date",
"fieldtype": "Datetime",
"in_list_view": 1,
@@ -207,7 +209,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-03 13:33:42.959387",
"modified": "2024-02-27 13:45:17.422435",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",

View File

@@ -1821,6 +1821,113 @@ class TestWorkOrder(FrappeTestCase):
valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
self.assertEqual(me.items[0].valuation_rate, valuation_rate)
def test_capcity_planning_for_workstation(self):
frappe.db.set_single_value(
"Manufacturing Settings",
{
"disable_capacity_planning": 0,
"capacity_planning_for_days": 1,
"mins_between_operations": 10,
},
)
properties = {"is_stock_item": 1, "valuation_rate": 100}
fg_item = make_item("Test FG Item For Capacity Planning", properties).name
rm_item = make_item("Test RM Item For Capacity Planning", properties).name
workstation = "Test Workstation For Capacity Planning"
if not frappe.db.exists("Workstation", workstation):
make_workstation(workstation=workstation, production_capacity=1)
operation = "Test Operation For Capacity Planning"
if not frappe.db.exists("Operation", operation):
make_operation(operation=operation, workstation=workstation)
bom_doc = make_bom(
item=fg_item,
source_warehouse="Stores - _TC",
raw_materials=[rm_item],
with_operations=1,
do_not_submit=True,
)
bom_doc.append(
"operations",
{"operation": operation, "time_in_mins": 1420, "hour_rate": 100, "workstation": workstation},
)
bom_doc.submit()
# 1st Work Order,
# Capacity to run parallel the operation 'Test Operation For Capacity Planning' is 2
wo_doc = make_wo_order_test_record(
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
)
wo_doc.submit()
job_cards = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name},
)
self.assertEqual(len(job_cards), 1)
# 2nd Work Order,
wo_doc = make_wo_order_test_record(
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
)
wo_doc.submit()
job_cards = frappe.get_all(
"Job Card",
filters={"work_order": wo_doc.name},
)
self.assertEqual(len(job_cards), 1)
# 3rd Work Order, capacity is full
wo_doc = make_wo_order_test_record(
production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
)
self.assertRaises(CapacityError, wo_doc.submit)
frappe.db.set_single_value(
"Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0}
)
def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)
operation_doc = frappe.get_doc(
{
"doctype": "Operation",
"name": kwargs.operation,
"workstation": kwargs.workstation,
}
)
operation_doc.insert()
return operation_doc
def make_workstation(**kwargs):
kwargs = frappe._dict(kwargs)
workstation_doc = frappe.get_doc(
{
"doctype": "Workstation",
"workstation_name": kwargs.workstation,
"workstation_type": kwargs.workstation_type,
"production_capacity": kwargs.production_capacity or 0,
"hour_rate": kwargs.hour_rate or 100,
}
)
workstation_doc.insert()
return workstation_doc
def prepare_boms_for_sub_assembly_test():
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):

View File

@@ -242,8 +242,12 @@ class WorkOrder(Document):
def calculate_operating_cost(self):
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
for d in self.get("operations"):
d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0)
d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0)
d.planned_operating_cost = flt(
flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost")
)
d.actual_operating_cost = flt(
flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0), d.precision("actual_operating_cost")
)
self.planned_operating_cost += flt(d.planned_operating_cost)
self.actual_operating_cost += flt(d.actual_operating_cost)
@@ -588,7 +592,6 @@ class WorkOrder(Document):
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row)
original_start_time = row.planned_start_time
job_card_doc = create_job_card(
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
)
@@ -597,11 +600,15 @@ class WorkOrder(Document):
row.planned_start_time = job_card_doc.scheduled_time_logs[-1].from_time
row.planned_end_time = job_card_doc.scheduled_time_logs[-1].to_time
if date_diff(row.planned_start_time, original_start_time) > plan_days:
if date_diff(row.planned_end_time, self.planned_start_date) > plan_days:
frappe.message_log.pop()
frappe.throw(
_("Unable to find the time slot in the next {0} days for the operation {1}.").format(
plan_days, row.operation
_(
"Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}."
).format(
plan_days,
row.operation,
get_link_to_form("Manufacturing Settings", "Manufacturing Settings"),
),
CapacityError,
)

View File

@@ -1,25 +1,28 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-08-12 12:44:27",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2018-02-13 04:58:51.549413",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Completed Work Orders",
"owner": "Administrator",
"query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) = `tabWork Order`.qty",
"ref_doctype": "Work Order",
"report_name": "Completed Work Orders",
"report_type": "Query Report",
"add_total_row": 0,
"columns": [],
"creation": "2013-08-12 12:44:27",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2024-02-21 14:35:14.301848",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Completed Work Orders",
"owner": "Administrator",
"prepared_report": 0,
"query": "SELECT\n `tabWork Order`.name as \"Work Order:Link/Work Order:200\",\n `tabWork Order`.creation as \"Date:Date:120\",\n `tabWork Order`.production_item as \"Item:Link/Item:150\",\n `tabWork Order`.qty as \"To Produce:Int:100\",\n `tabWork Order`.produced_qty as \"Produced:Int:100\",\n `tabWork Order`.company as \"Company:Link/Company:\"\nFROM\n `tabWork Order`\nWHERE\n `tabWork Order`.docstatus=1\n AND ifnull(`tabWork Order`.produced_qty,0) >= `tabWork Order`.qty",
"ref_doctype": "Work Order",
"report_name": "Completed Work Orders",
"report_type": "Query Report",
"roles": [
{
"role": "Manufacturing User"
},
},
{
"role": "Stock User"
}

View File

@@ -58,7 +58,7 @@ def get_data(filters):
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
data = frappe.get_all(
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
)
res = []

View File

@@ -264,6 +264,7 @@ execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Deta
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v14_0.delete_healthcare_doctypes
@@ -351,7 +352,9 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records

View File

@@ -0,0 +1,8 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
create_accounting_dimensions_for_doctype,
)
def execute():
create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation")
create_accounting_dimensions_for_doctype(doctype="Payment Reconciliation Allocation")

View File

@@ -0,0 +1,19 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE `tabStock Ledger Entry`
SET posting_datetime = DATE_FORMAT(timestamp(posting_date, posting_time), '%Y-%m-%d %H:%i:%s')
"""
)
drop_indexes()
def drop_indexes():
if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
return
frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`")

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
# nosemgrep
frappe.db.sql(
"""
DELETE FROM `tabAsset Movement Item`
WHERE parent NOT IN (SELECT name FROM `tabAsset Movement`)
"""
)

View File

@@ -35,6 +35,14 @@ frappe.ui.form.on("Project", {
};
});
frm.set_query("department", function (doc) {
return {
filters: {
"company": doc.company,
}
};
});
// sales order
frm.set_query('sales_order', function () {
var filters = {

View File

@@ -83,7 +83,7 @@ class Timesheet(Document):
def set_status(self):
self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)]
if self.per_billed == 100:
if flt(self.per_billed, self.precision("per_billed")) >= 100.0:
self.status = "Billed"
if self.sales_invoice:

View File

@@ -106,7 +106,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) {
debugger
var item = frappe.get_doc(cdt, cdn);
if (!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
@@ -166,7 +165,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if(this.frm.fields_dict["items"]) {
this["items_remove"] = this.calculate_net_weight;
this["items_remove"] = this.process_item_removal;
}
if(this.frm.fields_dict["recurring_print_format"]) {
@@ -398,7 +397,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
refresh() {
erpnext.toggle_naming_series();
erpnext.hide_company();
erpnext.hide_company(this.frm);
this.set_dynamic_labels();
this.setup_sms();
this.setup_quality_inspection();
@@ -1164,6 +1163,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if(!r.exc) {
me.apply_price_list(item, true)
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
}
}
@@ -1288,6 +1288,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
process_item_removal() {
this.frm.trigger("calculate_taxes_and_totals");
this.frm.trigger("calculate_net_weight");
}
calculate_net_weight(){
/* Calculate Total Net Weight then further applied shipping rule to calculate shipping charges.*/
var me = this;
@@ -1509,31 +1514,35 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
remove_pricing_rule_for_item(item) {
let me = this;
return this.frm.call({
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
args: {
pricing_rules: item.pricing_rules,
item_details: {
"doctype": item.doctype,
"name": item.name,
"item_code": item.item_code,
"pricing_rules": item.pricing_rules,
"parenttype": item.parenttype,
"parent": item.parent,
"price_list_rate": item.price_list_rate
// capture pricing rule before removing it to delete free items
let removed_pricing_rule = item.pricing_rules;
if (item.pricing_rules){
let me = this;
return this.frm.call({
method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.remove_pricing_rule_for_item",
args: {
pricing_rules: item.pricing_rules,
item_details: {
"doctype": item.doctype,
"name": item.name,
"item_code": item.item_code,
"pricing_rules": item.pricing_rules,
"parenttype": item.parenttype,
"parent": item.parent,
"price_list_rate": item.price_list_rate
},
item_code: item.item_code,
rate: item.price_list_rate,
},
item_code: item.item_code,
rate: item.price_list_rate,
},
callback: function(r) {
if (!r.exc && r.message) {
me.remove_pricing_rule(r.message);
me.calculate_taxes_and_totals();
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
callback: function(r) {
if (!r.exc && r.message) {
me.remove_pricing_rule(r.message, removed_pricing_rule);
me.calculate_taxes_and_totals();
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
}
}
}
});
});
}
}
apply_pricing_rule(item, calculate_taxes_and_totals) {
@@ -1785,7 +1794,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
});
}
remove_pricing_rule(item) {
remove_pricing_rule(item, removed_pricing_rule) {
let me = this;
const fields = ["discount_percentage",
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
@@ -1794,7 +1803,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let items = [];
me.frm.doc.items.forEach(d => {
if(d.item_code != item.remove_free_item || !d.is_free_item) {
// if same item was added a free item through a different pricing rule, keep it
if(d.item_code != item.remove_free_item || !d.is_free_item || removed_pricing_rule?.includes(d.pricing_rules)) {
items.push(d);
}
});

View File

@@ -26,21 +26,24 @@ $.extend(erpnext, {
}
},
hide_company: function() {
if(cur_frm.fields_dict.company) {
hide_company: function(frm) {
if(frm?.fields_dict.company) {
var companies = Object.keys(locals[":Company"] || {});
if(companies.length === 1) {
if(!cur_frm.doc.company) cur_frm.set_value("company", companies[0]);
cur_frm.toggle_display("company", false);
if(!frm.doc.company) frm.set_value("company", companies[0]);
frm.toggle_display("company", false);
} else if(erpnext.last_selected_company) {
if(!cur_frm.doc.company) cur_frm.set_value("company", erpnext.last_selected_company);
if(!frm.doc.company) frm.set_value("company", erpnext.last_selected_company);
}
}
},
is_perpetual_inventory_enabled: function(company) {
if(company) {
return frappe.get_doc(":Company", company).enable_perpetual_inventory
let company_local = locals[":Company"] && locals[":Company"][company];
if(company_local) {
return cint(company_local.enable_perpetual_inventory);
}
}
},

View File

@@ -297,11 +297,35 @@ class TestCustomer(FrappeTestCase):
if credit_limit > outstanding_amt:
set_credit_limit("_Test Customer", "_Test Company", credit_limit)
# Makes Sales invoice from Sales Order
so.save(ignore_permissions=True)
si = make_sales_invoice(so.name)
si.save(ignore_permissions=True)
self.assertRaises(frappe.ValidationError, make_sales_order)
def test_customer_credit_limit_after_submit(self):
from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
outstanding_amt = self.get_customer_outstanding_amount()
credit_limit = get_credit_limit("_Test Customer", "_Test Company")
if outstanding_amt <= 0.0:
item_qty = int((abs(outstanding_amt) + 200) / 100)
make_sales_order(qty=item_qty)
if credit_limit <= 0.0:
set_credit_limit("_Test Customer", "_Test Company", outstanding_amt + 100)
so = make_sales_order(rate=100, qty=1)
# Update qty in submitted Sales Order to trigger Credit Limit validation
fields = ["name", "item_code", "delivery_date", "conversion_factor", "qty", "rate", "uom", "idx"]
modified_item = frappe._dict()
for x in fields:
modified_item[x] = so.items[0].get(x)
modified_item["docname"] = so.items[0].name
modified_item["qty"] = 2
self.assertRaises(
frappe.ValidationError,
update_child_qty_rate,
so.doctype,
frappe.json.dumps([modified_item]),
so.name,
)
def test_customer_credit_limit_on_change(self):
outstanding_amt = self.get_customer_outstanding_amount()

View File

@@ -249,7 +249,8 @@ class SalesOrder(SellingController):
frappe.msgprint(
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
frappe.bold(so[0][0]), frappe.bold(self.po_no)
)
),
alert=True,
)
else:
frappe.throw(
@@ -513,6 +514,9 @@ class SalesOrder(SellingController):
def on_update(self):
pass
def on_update_after_submit(self):
self.check_credit_limit()
def before_update_after_submit(self):
self.validate_po()
self.validate_drop_ship()

View File

@@ -206,42 +206,36 @@ def prepare_data(
def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_field):
fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1)
dates = [fiscal_year.year_start_date, fiscal_year.year_end_date]
select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field)
child_table = "`tab{0}`".format(filters.get("doctype") + " Item")
parent_doc = frappe.qb.DocType(filters.get("doctype"))
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
sales_team = frappe.qb.DocType("Sales Team")
query = (
frappe.qb.from_(parent_doc)
.inner_join(child_doc)
.on(child_doc.parent == parent_doc.name)
.inner_join(sales_team)
.on(sales_team.parent == parent_doc.name)
.select(
child_doc.item_group,
(child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
(child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
sales_team.sales_person,
parent_doc[date_field],
)
.where(
(parent_doc.docstatus == 1)
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
)
)
if sales_field == "sales_person":
select_field = "`tabSales Team`.sales_person"
child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item")
cond = """`tabSales Team`.parent = `tab{0}`.name and
`tabSales Team`.sales_person in ({1}) """.format(
filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data))
)
query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
else:
cond = "`tab{0}`.{1} in ({2})".format(
filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data))
)
query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
return frappe.db.sql(
""" SELECT `tab{child_doc}`.item_group,
`tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount,
{select_field}, `tab{parent_doc}`.{date_field}
FROM `tab{parent_doc}`, {child_table}
WHERE
`tab{child_doc}`.parent = `tab{parent_doc}`.name
and `tab{parent_doc}`.docstatus = 1 and {cond}
and `tab{parent_doc}`.{date_field} between %s and %s""".format(
cond=cond,
date_field=date_field,
select_field=select_field,
child_table=child_table,
parent_doc=filters.get("doctype"),
child_doc=filters.get("doctype") + " Item",
),
tuple(sales_users_or_territory_data + dates),
as_dict=1,
)
return query.run(as_dict=True)
def get_parents_data(filters, partner_doctype):

View File

@@ -0,0 +1,84 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.sales_person_target_variance_based_on_item_group import (
execute,
)
class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
def setUp(self):
self.fiscal_year = get_fiscal_year(nowdate())[0]
def tearDown(self):
frappe.db.rollback()
def test_achieved_target_and_variance(self):
# Create a Target Distribution
distribution = frappe.new_doc("Monthly Distribution")
distribution.distribution_id = "Target Report Distribution"
distribution.fiscal_year = self.fiscal_year
distribution.get_months()
distribution.insert()
# Create sales people with targets
person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
# Create a Sales Order with 50-50 contribution
so = make_sales_order(
rate=1000,
qty=20,
do_not_submit=True,
)
so.set(
"sales_team",
[
{
"sales_person": person_1.name,
"allocated_percentage": 50,
"allocated_amount": 10000,
},
{
"sales_person": person_2.name,
"allocated_percentage": 50,
"allocated_amount": 10000,
},
],
)
so.submit()
# Check Achieved Target and Variance
result = execute(
frappe._dict(
{
"fiscal_year": self.fiscal_year,
"doctype": "Sales Order",
"period": "Yearly",
"target_on": "Quantity",
}
)
)[1]
row = frappe._dict(result[0])
self.assertSequenceEqual(
[flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
[50, 10, -40],
)
def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
sales_person = frappe.new_doc("Sales Person")
sales_person.sales_person_name = sales_person_name
sales_person.append(
"targets",
{
"fiscal_year": fiscal_year,
"target_qty": 50,
"target_amount": 30000,
"distribution_id": distribution_id,
},
)
return sales_person.insert()

View File

@@ -36,6 +36,7 @@ def execute(filters=None):
d.base_net_amount,
d.sales_person,
d.allocated_percentage,
(d.stock_qty * d.allocated_percentage / 100),
d.contribution_amt,
company_currency,
]
@@ -103,7 +104,7 @@ def get_columns(filters):
"fieldtype": "Link",
"width": 140,
},
{"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140},
{"label": _("SO Total Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140},
{
"label": _("Amount"),
"options": "currency",
@@ -119,6 +120,12 @@ def get_columns(filters):
"width": 140,
},
{"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140},
{
"label": _("Contribution Qty"),
"fieldname": "contribution_qty",
"fieldtype": "Float",
"width": 140,
},
{
"label": _("Contribution Amount"),
"options": "currency",

View File

@@ -95,7 +95,17 @@ def create_demo_record(doctype):
def make_transactions(company):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
start_date = get_fiscal_year(date=getdate())[1]
from erpnext.accounts.utils import FiscalYearError
try:
start_date = get_fiscal_year(date=getdate())[1]
except FiscalYearError:
# User might have setup fiscal year for previous or upcoming years
active_fiscal_years = frappe.db.get_all("Fiscal Year", filters={"disabled": 0}, as_list=1)
if active_fiscal_years:
start_date = frappe.db.get_value("Fiscal Year", active_fiscal_years[0][0], "year_start_date")
else:
frappe.throw(_("There are no active Fiscal Years for which Demo Data can be generated."))
for doctype in frappe.get_hooks("demo_transaction_doctypes"):
data = read_data_file_using_hooks(doctype)
@@ -159,6 +169,7 @@ def convert_order_to_invoices():
if i % 2 != 0:
payment = get_payment_entry(invoice.doctype, invoice.name)
payment.posting_date = order.transaction_date
payment.reference_no = invoice.name
payment.submit()

View File

@@ -20,7 +20,9 @@ default_mail_footer = """<div style="padding: 7px; text-align: right; color: #88
def after_install():
frappe.get_doc({"doctype": "Role", "role_name": "Analytics"}).insert()
if not frappe.db.exists("Role", "Analytics"):
frappe.get_doc({"doctype": "Role", "role_name": "Analytics"}).insert()
set_single_defaults()
create_print_setting_custom_fields()
add_all_roles_to("Administrator")

View File

@@ -8,9 +8,12 @@ from pypika import Order
class DeprecatedSerialNoValuation:
@deprecated
def calculate_stock_value_from_deprecarated_ledgers(self):
serial_nos = list(
filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos())
)
if not frappe.db.get_value(
"Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
):
return
serial_nos = self.get_serial_nos()
actual_qty = flt(self.sle.actual_qty)
@@ -25,23 +28,12 @@ class DeprecatedSerialNoValuation:
@deprecated
def get_incoming_value_for_serial_nos(self, serial_nos):
# get rate from serial nos within same company
all_serial_nos = frappe.get_all(
"Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
)
incoming_values = 0.0
for d in all_serial_nos:
if d.company == self.sle.company:
self.serial_no_incoming_rate[d.name] += flt(d.purchase_rate)
incoming_values += flt(d.purchase_rate)
# Get rate for serial nos which has been transferred to other company
invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company]
for serial_no in invalid_serial_nos:
for serial_no in serial_nos:
table = frappe.qb.DocType("Stock Ledger Entry")
incoming_rate = (
stock_ledgers = (
frappe.qb.from_(table)
.select(table.incoming_rate)
.select(table.incoming_rate, table.actual_qty, table.stock_value_difference)
.where(
(
(table.serial_no == serial_no)
@@ -51,15 +43,18 @@ class DeprecatedSerialNoValuation:
)
& (table.company == self.sle.company)
& (table.serial_and_batch_bundle.isnull())
& (table.actual_qty > 0)
& (table.is_cancelled == 0)
)
.orderby(table.posting_date, order=Order.desc)
.limit(1)
).run()
.orderby(table.posting_datetime, order=Order.desc)
).run(as_dict=1)
self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0
incoming_values += self.serial_no_incoming_rate[serial_no]
for sle in stock_ledgers:
self.serial_no_incoming_rate[serial_no] += (
flt(sle.incoming_rate)
if sle.actual_qty > 0
else (sle.stock_value_difference / sle.actual_qty) * -1
)
incoming_values += self.serial_no_incoming_rate[serial_no]
return incoming_values

View File

@@ -399,7 +399,12 @@ class DeliveryNote(SellingController):
elif self.issue_credit_note:
self.make_return_invoice()
self.make_bundle_using_old_serial_batch_fields()
for table_name in ["items", "packed_items"]:
if not self.get(table_name):
continue
self.make_bundle_using_old_serial_batch_fields(table_name)
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()

View File

@@ -1067,6 +1067,8 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(si2.items[1].qty, 1)
def test_delivery_note_bundle_with_batched_item(self):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0})
batched_item = make_item(
"_Test Batched Item",
@@ -1088,6 +1090,8 @@ class TestDeliveryNote(FrappeTestCase):
batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
self.assertTrue(batch_no)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1540,6 +1544,53 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(so.items[0].rate, rate)
self.assertEqual(dn.items[0].rate, so.items[0].rate)
def test_use_serial_batch_fields_for_packed_items(self):
bundle_item = make_item("Test _Packed Product Bundle Item ", {"is_stock_item": 0})
serial_item = make_item(
"Test _Packed Serial Item ",
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-TESTSERIAL-.#####"},
)
batch_item = make_item(
"Test _Packed Batch Item ",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_no_series": "BATCH-TESTSERIAL-.#####",
"create_new_batch": 1,
},
)
make_product_bundle(parent=bundle_item.name, items=[serial_item.name, batch_item.name])
item_details = {}
for item in [serial_item, batch_item]:
se = make_stock_entry(
item_code=item.name, target="_Test Warehouse - _TC", qty=5, basic_rate=100
)
item_details[item.name] = se.items[0].serial_and_batch_bundle
dn = create_delivery_note(item_code=bundle_item.name, qty=1, do_not_submit=True)
serial_no = ""
for row in dn.packed_items:
row.use_serial_batch_fields = 1
if row.item_code == serial_item.name:
serial_and_batch_bundle = item_details[serial_item.name]
row.serial_no = get_serial_nos_from_bundle(serial_and_batch_bundle)[3]
serial_no = row.serial_no
else:
serial_and_batch_bundle = item_details[batch_item.name]
row.batch_no = get_batch_from_bundle(serial_and_batch_bundle)
dn.submit()
dn.load_from_db()
for row in dn.packed_items:
self.assertTrue(row.serial_no or row.batch_no)
self.assertTrue(row.serial_and_batch_bundle)
if row.serial_no:
self.assertEqual(row.serial_no, serial_no)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -1,187 +1,77 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:28:02",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"actions": [],
"creation": "2013-02-22 01:28:02",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"receipt_document_type",
"receipt_document",
"supplier",
"col_break1",
"posting_date",
"grand_total"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "receipt_document_type",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Receipt Document Type",
"length": 0,
"no_copy": 0,
"options": "\nPurchase Invoice\nPurchase Receipt",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "receipt_document_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Receipt Document Type",
"options": "\nPurchase Invoice\nPurchase Receipt",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "receipt_document",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Receipt Document",
"length": 0,
"no_copy": 0,
"oldfieldname": "purchase_receipt_no",
"oldfieldtype": "Link",
"options": "receipt_document_type",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "220px",
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "receipt_document",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Receipt Document",
"oldfieldname": "purchase_receipt_no",
"oldfieldtype": "Link",
"options": "receipt_document_type",
"print_width": "220px",
"reqd": 1,
"width": "220px"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "supplier",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Supplier",
"length": 0,
"no_copy": 0,
"options": "Supplier",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier",
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"width": "50%"
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "posting_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Posting Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "grand_total",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Grand Total",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "grand_total",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Grand Total",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Purchase Receipt",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-02-26 18:41:06.281750",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Purchase Receipt",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}

View File

@@ -199,6 +199,7 @@ frappe.ui.form.on('Material Request', {
get_item_data: function(frm, item, overwrite_warehouse=false) {
if (item && !item.item_code) { return; }
frappe.call({
method: "erpnext.stock.get_item_details.get_item_details",
args: {
@@ -225,20 +226,22 @@ frappe.ui.form.on('Material Request', {
},
callback: function(r) {
const d = item;
const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty'];
const allow_to_change_fields = ['actual_qty', 'projected_qty', 'min_order_qty', 'item_name', 'description', 'stock_uom', 'uom', 'conversion_factor', 'stock_qty'];
if(!r.exc) {
$.each(r.message, function(key, value) {
if(!d[key] || qty_fields.includes(key)) {
if(!d[key] || allow_to_change_fields.includes(key)) {
d[key] = value;
}
});
if (d.price_list_rate != r.message.price_list_rate) {
d.rate = 0.0;
d.price_list_rate = r.message.price_list_rate;
frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate);
}
refresh_field("items");
}
}
});
@@ -435,7 +438,7 @@ frappe.ui.form.on("Material Request Item", {
frm.events.get_item_data(frm, item, false);
},
rate: function(frm, doctype, name) {
rate(frm, doctype, name) {
const item = locals[doctype][name];
item.amount = flt(item.qty) * flt(item.rate);
frappe.model.set_value(doctype, name, "amount", item.amount);

View File

@@ -198,12 +198,14 @@
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate",
"options": "Company:company:default_currency",
"print_hide": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -489,7 +491,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-08 16:30:56.137858",
"modified": "2024-02-26 18:30:03.684872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",

View File

@@ -227,6 +227,9 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse)
pi_row.actual_qty = flt(bin.get("actual_qty"))
pi_row.projected_qty = flt(bin.get("projected_qty"))
pi_row.use_serial_batch_fields = frappe.db.get_single_value(
"Stock Settings", "use_serial_batch_fields"
)
def update_packed_item_price_data(pi_row, item_data, doc):

View File

@@ -2200,6 +2200,10 @@ class TestPurchaseReceipt(FrappeTestCase):
create_stock_reconciliation,
)
frappe.db.set_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 0
)
item_code = make_item(
"_Test Use Serial Fields Item Serial Item",
properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"},
@@ -2280,6 +2284,99 @@ class TestPurchaseReceipt(FrappeTestCase):
serial_no_status = frappe.db.get_value("Serial No", sn, "status")
self.assertTrue(serial_no_status != "Active")
frappe.db.set_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 1
)
def test_sle_qty_after_transaction(self):
item = make_item(
"_Test Item Qty After Transaction",
properties={"is_stock_item": 1, "valuation_method": "FIFO"},
).name
posting_date = today()
posting_time = nowtime()
# Step 1: Create Purchase Receipt
pr = make_purchase_receipt(
item_code=item,
qty=1,
rate=100,
posting_date=posting_date,
posting_time=posting_time,
do_not_save=1,
)
for i in range(9):
pr.append(
"items",
{
"item_code": item,
"qty": 1,
"rate": 100,
"warehouse": pr.items[0].warehouse,
"cost_center": pr.items[0].cost_center,
"expense_account": pr.items[0].expense_account,
"uom": pr.items[0].uom,
"stock_uom": pr.items[0].stock_uom,
"conversion_factor": pr.items[0].conversion_factor,
},
)
self.assertEqual(len(pr.items), 10)
pr.save()
pr.submit()
data = frappe.get_all(
"Stock Ledger Entry",
fields=["qty_after_transaction", "creation", "posting_datetime"],
filters={"voucher_no": pr.name, "is_cancelled": 0},
order_by="creation",
)
for index, d in enumerate(data):
self.assertEqual(d.qty_after_transaction, 1 + index)
# Step 2: Create Purchase Receipt
pr = make_purchase_receipt(
item_code=item,
qty=1,
rate=100,
posting_date=posting_date,
posting_time=posting_time,
do_not_save=1,
)
for i in range(9):
pr.append(
"items",
{
"item_code": item,
"qty": 1,
"rate": 100,
"warehouse": pr.items[0].warehouse,
"cost_center": pr.items[0].cost_center,
"expense_account": pr.items[0].expense_account,
"uom": pr.items[0].uom,
"stock_uom": pr.items[0].stock_uom,
"conversion_factor": pr.items[0].conversion_factor,
},
)
self.assertEqual(len(pr.items), 10)
pr.save()
pr.submit()
data = frappe.get_all(
"Stock Ledger Entry",
fields=["qty_after_transaction", "creation", "posting_datetime"],
filters={"voucher_no": pr.name, "is_cancelled": 0},
order_by="creation",
)
for index, d in enumerate(data):
self.assertEqual(d.qty_after_transaction, 11 + index)
def test_auto_set_batch_based_on_bundle(self):
item_code = make_item(
"_Test Auto Set Batch Based on Bundle",
@@ -2290,7 +2387,9 @@ class TestPurchaseReceipt(FrappeTestCase):
},
).name
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
frappe.db.set_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 0
)
pr = make_purchase_receipt(
item_code=item_code,
@@ -2302,7 +2401,44 @@ class TestPurchaseReceipt(FrappeTestCase):
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
self.assertEqual(pr.items[0].batch_no, batch_no)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
frappe.db.set_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 1
)
def test_pr_billed_amount_against_return_entry(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
# Create a Purchase Receipt and Fully Bill it
pr = make_purchase_receipt(qty=10)
pi = make_pi_from_pr(pr.name)
pi.insert()
pi.submit()
# Debit Note - 50% Qty & enable updating PR billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_receipt = 1
pi_return.submit()
# Check if the billed amount reduced
pr.reload()
self.assertEqual(pr.per_billed, 50)
pi_return.reload()
pi_return.cancel()
# Debit Note - 50% Qty & disable updating PR billed amount
pi_return = make_debit_note(pi.name)
pi_return.items[0].qty = -5
pi_return.update_billed_amount_in_purchase_receipt = 0
pi_return.submit()
# Check if the billed amount stayed the same
pr.reload()
self.assertEqual(pr.per_billed, 100)
def prepare_data_for_internal_transfer():

View File

@@ -272,6 +272,7 @@ def on_doctype_update():
def repost(doc):
try:
frappe.flags.through_repost_item_valuation = True
if not frappe.db.exists("Repost Item Valuation", doc.name):
return

View File

@@ -257,9 +257,9 @@ class SerialandBatchBundle(Document):
if sn_obj.batch_avg_rate.get(d.batch_no):
d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
available_qty = flt(sn_obj.available_qty.get(d.batch_no))
available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty"))
if self.docstatus == 1:
available_qty += flt(d.qty)
available_qty += flt(d.qty, d.precision("qty"))
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty)
@@ -2118,7 +2118,7 @@ def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, ba
make_serial_no(serial_no, item_code)
if batch_no and frappe.db.exists("Batch", batch_no):
if batch_no and not frappe.db.exists("Batch", batch_no):
if type_of_transaction != "Inward":
frappe.throw(_("Batch No {0} does not exists").format(batch_no))

View File

@@ -5,7 +5,7 @@ import json
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
from frappe.utils import flt, nowtime, today
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
@@ -191,6 +191,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.submit()
doc.reload()
bundle_doc = make_serial_batch_bundle(
{
@@ -539,6 +540,110 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertRaises(frappe.exceptions.ValidationError, pr2.save)
def test_serial_no_valuation_for_legacy_ledgers(self):
sn_item = make_item(
"Test Serial No Valuation for Legacy Ledgers",
properties={"has_serial_no": 1, "serial_no_series": "SNN-TSNVL.-#####"},
).name
serial_nos = []
for serial_no in [f"{sn_item}-0001", f"{sn_item}-0002"]:
if not frappe.db.exists("Serial No", serial_no):
sn_doc = frappe.get_doc(
{
"doctype": "Serial No",
"serial_no": serial_no,
"item_code": sn_item,
}
).insert(ignore_permissions=True)
serial_nos.append(serial_no)
frappe.flags.ignore_serial_batch_bundle_validation = True
qty_after_transaction = 0.0
stock_value = 0.0
for row in [{"qty": 2, "rate": 100}, {"qty": -2, "rate": 100}, {"qty": 2, "rate": 200}]:
row = frappe._dict(row)
qty_after_transaction += row.qty
stock_value += row.rate * row.qty
doc = frappe.get_doc(
{
"doctype": "Stock Ledger Entry",
"posting_date": today(),
"posting_time": nowtime(),
"incoming_rate": row.rate if row.qty > 0 else 0,
"qty_after_transaction": qty_after_transaction,
"stock_value_difference": row.rate * row.qty,
"stock_value": stock_value,
"valuation_rate": row.rate,
"actual_qty": row.qty,
"item_code": sn_item,
"warehouse": "_Test Warehouse - _TC",
"serial_no": "\n".join(serial_nos),
"company": "_Test Company",
}
)
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.submit()
for sn in serial_nos:
sn_doc = frappe.get_doc("Serial No", sn)
if row.qty > 0:
sn_doc.db_set("warehouse", "_Test Warehouse - _TC")
else:
sn_doc.db_set("warehouse", "")
frappe.flags.ignore_serial_batch_bundle_validation = False
se = make_stock_entry(
item_code=sn_item,
qty=2,
source="_Test Warehouse - _TC",
serial_no="\n".join(serial_nos),
use_serial_batch_fields=True,
do_not_submit=True,
)
se.save()
se.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"},
"stock_value_difference",
)
self.assertEqual(flt(stock_value_difference, 2), 400.0 * -1)
se = make_stock_entry(
item_code=sn_item,
qty=1,
rate=353,
target="_Test Warehouse - _TC",
)
serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0]
se = make_stock_entry(
item_code=sn_item,
qty=1,
source="_Test Warehouse - _TC",
serial_no=serial_no,
use_serial_batch_fields=True,
)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"},
"stock_value_difference",
)
self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1)
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@@ -68,7 +68,7 @@
{
"fieldname": "incoming_rate",
"fieldtype": "Float",
"label": "Incoming Rate",
"label": "Valuation Rate",
"no_copy": 1,
"read_only": 1,
"read_only_depends_on": "eval:parent.type_of_transaction == \"Outward\""
@@ -76,6 +76,7 @@
{
"fieldname": "outgoing_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Outgoing Rate",
"no_copy": 1,
"read_only": 1
@@ -95,6 +96,7 @@
"default": "0",
"fieldname": "is_outward",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Outward",
"read_only": 1
},
@@ -120,7 +122,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-12-10 19:47:48.227772",
"modified": "2024-02-23 12:44:18.054270",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Entry",

View File

@@ -943,7 +943,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
if (this.frm.doc.docstatus===1 && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) {
this.show_general_ledger();
}
erpnext.hide_company();
erpnext.hide_company(this.frm);
erpnext.utils.add_item(this.frm);
}

View File

@@ -1028,7 +1028,7 @@ class StockEntry(StockController):
already_picked_serial_nos = []
for row in self.items:
if row.use_serial_batch_fields and (row.serial_no or row.batch_no):
if row.use_serial_batch_fields:
continue
if not row.s_warehouse:
@@ -1897,6 +1897,7 @@ class StockEntry(StockController):
return
id = create_serial_and_batch_bundle(
self,
row,
frappe._dict(
{
@@ -2167,7 +2168,7 @@ class StockEntry(StockController):
"to_warehouse": "",
"qty": qty,
"item_name": item.item_name,
"serial_and_batch_bundle": create_serial_and_batch_bundle(row, item, "Outward"),
"serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward"),
"description": item.description,
"stock_uom": item.stock_uom,
"expense_account": item.expense_account,
@@ -2545,6 +2546,7 @@ class StockEntry(StockController):
row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]})
id = create_serial_and_batch_bundle(
self,
row,
frappe._dict(
{
@@ -3068,7 +3070,7 @@ def get_stock_entry_data(work_order):
return data
def create_serial_and_batch_bundle(row, child, type_of_transaction=None):
def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=None):
item_details = frappe.get_cached_value(
"Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
@@ -3086,6 +3088,8 @@ def create_serial_and_batch_bundle(row, child, type_of_transaction=None):
"item_code": child.item_code,
"warehouse": child.warehouse,
"type_of_transaction": type_of_transaction,
"posting_date": parent_doc.posting_date,
"posting_time": parent_doc.posting_time,
}
)

View File

@@ -1602,24 +1602,22 @@ class TestStockEntry(FrappeTestCase):
item_code = "Test Negative Item - 001"
item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
make_stock_entry(
se1 = make_stock_entry(
item_code=item_code,
posting_date=add_days(today(), -3),
posting_time="00:00:00",
purpose="Material Receipt",
target="_Test Warehouse - _TC",
qty=10,
to_warehouse="_Test Warehouse - _TC",
do_not_save=True,
)
make_stock_entry(
se2 = make_stock_entry(
item_code=item_code,
posting_date=today(),
posting_time="00:00:00",
purpose="Material Receipt",
source="_Test Warehouse - _TC",
qty=8,
from_warehouse="_Test Warehouse - _TC",
do_not_save=True,
)
sr_doc = create_stock_reconciliation(

View File

@@ -294,7 +294,7 @@
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"fieldtype": "Text",
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
@@ -610,7 +610,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-04 16:16:47.606270",
"modified": "2024-02-25 15:58:40.982582",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -54,7 +54,7 @@ class StockEntryDetail(Document):
sample_quantity: DF.Int
sco_rm_detail: DF.Data | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
serial_no: DF.Text | None
set_basic_rate_manually: DF.Check
ste_detail: DF.Data | None
stock_uom: DF.Link

View File

@@ -11,6 +11,7 @@
"warehouse",
"posting_date",
"posting_time",
"posting_datetime",
"is_adjustment_entry",
"auto_created_serial_and_batch_bundle",
"column_break_6",
@@ -100,7 +101,6 @@
"oldfieldtype": "Date",
"print_width": "100px",
"read_only": 1,
"search_index": 1,
"width": "100px"
},
{
@@ -253,7 +253,6 @@
"options": "Company",
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
{
@@ -348,6 +347,11 @@
"fieldname": "auto_created_serial_and_batch_bundle",
"fieldtype": "Check",
"label": "Auto Created Serial and Batch Bundle"
},
{
"fieldname": "posting_datetime",
"fieldtype": "Datetime",
"label": "Posting Datetime"
}
],
"hide_toolbar": 1,
@@ -356,7 +360,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-14 16:47:39.791967",
"modified": "2024-02-07 09:18:13.999231",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -51,6 +51,7 @@ class StockLedgerEntry(Document):
item_code: DF.Link | None
outgoing_rate: DF.Currency
posting_date: DF.Date | None
posting_datetime: DF.Datetime | None
posting_time: DF.Time | None
project: DF.Link | None
qty_after_transaction: DF.Float
@@ -92,6 +93,12 @@ class StockLedgerEntry(Document):
self.validate_with_last_transaction_posting_time()
self.validate_inventory_dimension_negative_stock()
def set_posting_datetime(self):
from erpnext.stock.utils import get_combine_datetime
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
self.db_set("posting_datetime", self.posting_datetime)
def validate_inventory_dimension_negative_stock(self):
if self.is_cancelled:
return
@@ -162,6 +169,7 @@ class StockLedgerEntry(Document):
return inv_dimension_dict
def on_submit(self):
self.set_posting_datetime()
self.check_stock_frozen_date()
# Added to handle few test cases where serial_and_batch_bundles are not required
@@ -330,9 +338,7 @@ class StockLedgerEntry(Document):
def on_doctype_update():
frappe.db.add_index(
"Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index"
)
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"])

View File

@@ -2,6 +2,7 @@
# See license.txt
import json
import time
from uuid import uuid4
import frappe
@@ -1077,7 +1078,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
frappe.qb.from_(sle)
.select("qty_after_transaction")
.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
).run(pluck=True)
@@ -1154,6 +1155,89 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
except Exception as e:
self.fail("Double processing of qty for clashing timestamp.")
def test_previous_sle_with_clashed_timestamp(self):
item = make_item().name
warehouse = "_Test Warehouse - _TC"
reciept1 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00",
)
time.sleep(3)
reciept2 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=5,
posting_date="2021-01-01",
rate=10,
posting_time="02:00:00.1234",
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept1.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 100)
self.assertEqual(sle[0].actual_qty, 100)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept2.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 105)
self.assertEqual(sle[0].actual_qty, 5)
def test_backdated_sle_with_same_timestamp(self):
item = make_item().name
warehouse = "_Test Warehouse - _TC"
reciept1 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=5,
posting_date="2021-01-01",
rate=10,
posting_time="02:00:00.1234",
)
time.sleep(3)
# backdated entry with same timestamp but different ms part
reciept2 = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=100,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00",
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept1.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 5)
self.assertEqual(sle[0].actual_qty, 5)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": reciept2.name},
fields=["qty_after_transaction", "actual_qty"],
)
self.assertEqual(sle[0].qty_after_transaction, 105)
self.assertEqual(sle[0].actual_qty, 100)
@change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
def test_transfer_invariants(self):
"""Extact stock value should be transferred."""

View File

@@ -906,8 +906,13 @@ class StockReconciliation(StockController):
def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if allow_negative_stock:
return True
if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items):
if any(
((d.serial_and_batch_bundle or d.batch_no) and flt(d.qty) == flt(d.current_qty))
for d in self.items
):
allow_negative_stock = True
return allow_negative_stock

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt
from frappe.utils import cint, flt, nowdate, nowtime
from erpnext.stock.utils import get_or_make_bin, get_stock_balance
@@ -866,6 +866,8 @@ def get_ssb_bundle_for_voucher(sre: dict) -> object:
bundle = frappe.new_doc("Serial and Batch Bundle")
bundle.type_of_transaction = "Outward"
bundle.voucher_type = "Delivery Note"
bundle.posting_date = nowdate()
bundle.posting_time = nowtime()
for field in ("item_code", "warehouse", "has_serial_no", "has_batch_no"):
setattr(bundle, field, sre[field])

View File

@@ -51,6 +51,7 @@
"use_naming_series",
"naming_series_prefix",
"use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle",
"stock_planning_tab",
"auto_material_request",
"auto_indent",
@@ -424,9 +425,18 @@
},
{
"default": "1",
"description": "On submission of the stock transaction, system will auto create the Serial and Batch Bundle based on the Serial No / Batch fields.",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial / Batch Fields"
},
{
"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"
}
],
"icon": "icon-cog",
@@ -434,7 +444,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-02-04 12:01:31.931864",
"modified": "2024-02-25 16:32:01.084453",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -39,6 +39,7 @@ class StockSettings(Document):
clean_description_html: DF.Check
default_warehouse: DF.Link | None
disable_serial_no_and_batch_selector: DF.Check
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
enable_stock_reservation: DF.Check
item_group: DF.Link | None
item_naming_by: DF.Literal["Item Code", "Naming Series"]

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder import Field
from frappe.query_builder.functions import CombineDatetime, Min
from frappe.query_builder.functions import Min
from frappe.utils import add_days, getdate, today
import erpnext
@@ -75,7 +75,7 @@ def get_data(report_filters):
& (sle.company == report_filters.company)
& (sle.is_cancelled == 0)
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation)
.orderby(sle.posting_datetime, sle.creation)
).run(as_dict=True)
for d in data:

View File

@@ -213,13 +213,11 @@ def get_stock_ledger_entries(filters, items):
query = (
frappe.qb.from_(sle)
.force_index("posting_sort_index")
.left_join(sle2)
.on(
(sle.item_code == sle2.item_code)
& (sle.warehouse == sle2.warehouse)
& (sle.posting_date < sle2.posting_date)
& (sle.posting_time < sle2.posting_time)
& (sle.posting_datetime < sle2.posting_datetime)
& (sle.name < sle2.name)
)
.select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company)

View File

@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, TypedDict
import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Coalesce, CombineDatetime
from frappe.query_builder.functions import Coalesce
from frappe.utils import add_days, cint, date_diff, flt, getdate
from frappe.utils.nestedset import get_descendants_of
@@ -300,7 +300,7 @@ class StockBalanceReport(object):
item_table.item_name,
)
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
.orderby(sle.actual_qty)
)

View File

@@ -345,7 +345,7 @@ def get_stock_ledger_entries(filters, items):
frappe.qb.from_(sle)
.select(
sle.item_code,
CombineDatetime(sle.posting_date, sle.posting_time).as_("date"),
sle.posting_datetime.as_("date"),
sle.warehouse,
sle.posting_date,
sle.posting_time,

View File

@@ -142,9 +142,11 @@ class SerialBatchBundle:
"serial_and_batch_bundle": sn_doc.name,
}
if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
if not frappe.db.get_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
):
if sn_doc.has_serial_no:
values_to_update["serial_no"] = "\n".join(cstr(d.serial_no) for d in sn_doc.entries)
values_to_update["serial_no"] = ",".join(cstr(d.serial_no) for d in sn_doc.entries)
elif sn_doc.has_batch_no and len(sn_doc.entries) == 1:
values_to_update["batch_no"] = sn_doc.entries[0].batch_no

View File

@@ -9,7 +9,7 @@ from typing import Optional, Set, Tuple
import frappe
from frappe import _, scrub
from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
cstr,
@@ -33,6 +33,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor
get_sre_reserved_serial_nos_details,
)
from erpnext.stock.utils import (
get_combine_datetime,
get_incoming_outgoing_rate_for_cancel,
get_incoming_rate,
get_or_make_bin,
@@ -95,6 +96,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
args = sle_doc.as_dict()
args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time)
if sle.get("voucher_type") == "Stock Reconciliation":
# preserve previous_qty_after_transaction for qty reposting
@@ -616,12 +618,14 @@ class update_entries_after(object):
self.process_sle(sle)
def get_sle_against_current_voucher(self):
self.args["time_format"] = "%H:%i:%s"
self.args["posting_datetime"] = get_combine_datetime(
self.args.posting_date, self.args.posting_time
)
return frappe.db.sql(
"""
select
*, timestamp(posting_date, posting_time) as "timestamp"
*, posting_datetime as "timestamp"
from
`tabStock Ledger Entry`
where
@@ -629,8 +633,7 @@ class update_entries_after(object):
and warehouse = %(warehouse)s
and is_cancelled = 0
and (
posting_date = %(posting_date)s and
time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
posting_datetime = %(posting_datetime)s
)
order by
creation ASC
@@ -1399,11 +1402,11 @@ class update_entries_after(object):
def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
"""get stock ledger entries filtered by specific posting datetime conditions"""
args["time_format"] = "%H:%i:%s"
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
args["posting_datetime"] = "1900-01-01 00:00:00"
if not args.get("posting_datetime"):
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
voucher_condition = ""
if exclude_current_voucher:
@@ -1412,23 +1415,20 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
sle = frappe.db.sql(
"""
select *, timestamp(posting_date, posting_time) as "timestamp"
select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
{voucher_condition}
and (
posting_date < %(posting_date)s or
(
posting_date = %(posting_date)s and
time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
)
posting_datetime {operator} %(posting_datetime)s
)
order by timestamp(posting_date, posting_time) desc, creation desc
order by posting_datetime desc, creation desc
limit 1
for update""".format(
operator=operator, voucher_condition=voucher_condition
operator=operator,
voucher_condition=voucher_condition,
),
args,
as_dict=1,
@@ -1469,9 +1469,7 @@ def get_stock_ledger_entries(
extra_cond=None,
):
"""get stock ledger entries filtered by specific posting datetime conditions"""
conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
operator
)
conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
if previous_sle.get("warehouse"):
conditions += " and warehouse = %(warehouse)s"
elif previous_sle.get("warehouse_condition"):
@@ -1497,9 +1495,11 @@ def get_stock_ledger_entries(
)
if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01"
if not previous_sle.get("posting_time"):
previous_sle["posting_time"] = "00:00"
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"]
)
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
@@ -1509,12 +1509,12 @@ def get_stock_ledger_entries(
return frappe.db.sql(
"""
select *, timestamp(posting_date, posting_time) as "timestamp"
select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry`
where item_code = %%(item_code)s
and is_cancelled = 0
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
order by posting_datetime %(order)s, creation %(order)s
%(limit)s %(for_update)s"""
% {
"conditions": conditions,
@@ -1540,7 +1540,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
"posting_date",
"posting_time",
"voucher_detail_no",
"timestamp(posting_date, posting_time) as timestamp",
"posting_datetime as timestamp",
],
as_dict=1,
)
@@ -1552,13 +1552,10 @@ def get_batch_incoming_rate(
sle = frappe.qb.DocType("Stock Ledger Entry")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time
)
timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time)
if creation:
timestamp_condition |= (
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(posting_date, posting_time)
sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
) & (sle.creation < creation)
batch_details = (
@@ -1639,7 +1636,7 @@ def get_valuation_rate(
AND valuation_rate >= 0
AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""",
order by posting_datetime desc, name desc limit 1""",
(item_code, warehouse, voucher_no, voucher_type),
):
return flt(last_valuation_rate[0][0])
@@ -1698,7 +1695,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
datetime_limit_condition = ""
qty_shift = args.actual_qty
args["time_format"] = "%H:%i:%s"
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
# find difference/shift in qty caused by stock reconciliation
if args.voucher_type == "Stock Reconciliation":
@@ -1708,8 +1705,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail:
detail = next_stock_reco_detail[0]
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
frappe.db.sql(
@@ -1722,13 +1717,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (
posting_date > %(posting_date)s or
(
posting_date = %(posting_date)s and
time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
)
posting_datetime > %(posting_datetime)s
)
{datetime_limit_condition}
{datetime_limit_condition}
""",
args,
)
@@ -1785,20 +1776,11 @@ def get_next_stock_reco(kwargs):
& (sle.voucher_no != kwargs.get("voucher_no"))
& (sle.is_cancelled == 0)
& (
(
CombineDatetime(sle.posting_date, sle.posting_time)
> CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
| (
(
CombineDatetime(sle.posting_date, sle.posting_time)
== CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
& (sle.creation > kwargs.get("creation"))
)
sle.posting_datetime
>= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
)
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
.limit(1)
)
@@ -1810,11 +1792,13 @@ def get_next_stock_reco(kwargs):
def get_datetime_limit_condition(detail):
posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
return f"""
and
(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
(posting_datetime < '{posting_datetime}'
or (
timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
posting_datetime = '{posting_datetime}'
and creation < '{detail.creation}'
)
)"""
@@ -1888,10 +1872,10 @@ def get_future_sle_with_negative_qty(args):
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and posting_datetime >= %(posting_datetime)s
and is_cancelled = 0
and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
order by posting_datetime asc
limit 1
""",
args,
@@ -1904,20 +1888,20 @@ def get_future_sle_with_negative_batch_qty(args):
"""
with batch_ledger as (
select
posting_date, posting_time, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
from `tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and batch_no=%(batch_no)s
and is_cancelled = 0
order by posting_date, posting_time, creation
order by posting_datetime, creation
)
select * from batch_ledger
where
cumulative_total < 0.0
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and posting_datetime >= %(posting_datetime)s
limit 1
""",
args,
@@ -2059,6 +2043,7 @@ def is_internal_transfer(sle):
def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
table = frappe.qb.DocType("Stock Ledger Entry")
posting_datetime = get_combine_datetime(posting_date, posting_time)
query = (
frappe.qb.from_(table)
@@ -2067,10 +2052,7 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
(table.is_cancelled == 0)
& (table.item_code == item_code)
& (table.warehouse == warehouse)
& (
(table.posting_date < posting_date)
| ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
)
& (table.posting_datetime <= posting_datetime)
)
)

View File

@@ -8,7 +8,7 @@ from typing import Dict, Optional
import frappe
from frappe import _
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
import erpnext
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
@@ -657,3 +657,18 @@ def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Option
):
scan_result.update(item_info)
return scan_result
def get_combine_datetime(posting_date, posting_time):
import datetime
if isinstance(posting_date, str):
posting_date = getdate(posting_date)
if isinstance(posting_time, str):
posting_time = get_time(posting_time)
if isinstance(posting_time, datetime.timedelta):
posting_time = (datetime.datetime.min + posting_time).time()
return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0)

View File

@@ -643,10 +643,6 @@ class TestSubcontractingReceipt(FrappeTestCase):
)
scr = make_subcontracting_receipt(sco.name)
scr.save()
for row in scr.supplied_items:
self.assertNotEqual(row.rate, 300.00)
self.assertFalse(row.serial_and_batch_bundle)
scr.submit()
scr.reload()

View File

@@ -39,3 +39,6 @@ force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
indent = "\t"
[tool.bench.frappe-dependencies]
frappe = ">=15.10.0,<16.0.0"