mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +00:00
Merge branch 'version-13-hotfix' into doc_currency_override
This commit is contained in:
10
.github/workflows/patch.yml
vendored
10
.github/workflows/patch.yml
vendored
@@ -5,9 +5,14 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
types: [opened, unlabeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: patch-mariadb-v13-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
@@ -25,6 +30,11 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check for merge conficts label
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
|
||||
run: |
|
||||
echo "Remove merge conflicts and remove conflict label to run CI"
|
||||
exit 1
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
44
.github/workflows/server-tests.yml
vendored
44
.github/workflows/server-tests.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
types: [opened, unlabeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
@@ -12,6 +13,10 @@ on:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-v13-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
@@ -35,6 +40,12 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check for merge conficts label
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
|
||||
run: |
|
||||
echo "Remove merge conflicts and remove conflict label to run CI"
|
||||
exit 1
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -89,39 +100,8 @@ jobs:
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
|
||||
- name: Run Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
|
||||
env:
|
||||
TYPE: server
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload Coverage Data
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
pip3 install coveralls==3.0.1
|
||||
coveralls
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
|
||||
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
|
||||
COVERALLS_PARALLEL: true
|
||||
|
||||
coveralls:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
container: python:3-slim
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Coveralls Finished
|
||||
run: |
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
pip3 install coveralls==3.0.1
|
||||
coveralls --finish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/ui-tests.yml
vendored
4
.github/workflows/ui-tests.yml
vendored
@@ -6,6 +6,10 @@ on:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ui-v13-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
@@ -17,6 +17,7 @@ from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
from six import string_types
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
|
||||
class BankStatementImport(DataImport):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -96,6 +97,18 @@ def download_errored_template(data_import_name):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
for i, row in enumerate(raw_data):
|
||||
if all(v in INVALID_VALUES for v in row):
|
||||
# empty row
|
||||
continue
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
"""This method runs in background job"""
|
||||
|
||||
@@ -105,7 +118,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
file = import_file_path if import_file_path else google_sheets_url
|
||||
|
||||
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
|
||||
data = import_file.raw_data
|
||||
|
||||
data = parse_data_from_template(import_file.raw_data)
|
||||
|
||||
if import_file_path:
|
||||
add_bank_account(data, bank_account)
|
||||
|
||||
@@ -45,6 +45,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
|
||||
from erpnext.stock.utils import calculate_mapped_packed_items_return
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@@ -745,8 +746,11 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def update_packing_list(self):
|
||||
if cint(self.update_stock) == 1:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
if cint(self.is_return) and self.return_against:
|
||||
calculate_mapped_packed_items_return(self)
|
||||
else:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
else:
|
||||
self.set('packed_items', [])
|
||||
|
||||
|
||||
@@ -2165,9 +2165,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48],
|
||||
["2021-06-30", 20000.0, 21311.48],
|
||||
["2021-09-30", 5041.1, 26352.58]
|
||||
["2020-06-30", 1366.12, 1366.12],
|
||||
["2021-06-30", 20000.0, 21366.12],
|
||||
["2021-09-30", 5041.1, 26407.22]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@@ -2215,12 +2215,12 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48, True],
|
||||
["2021-06-30", 20000.0, 21311.48, True],
|
||||
["2022-06-30", 20000.0, 41311.48, False],
|
||||
["2023-06-30", 20000.0, 61311.48, False],
|
||||
["2024-06-30", 20000.0, 81311.48, False],
|
||||
["2025-06-06", 18688.52, 100000.0, False]
|
||||
["2020-06-30", 1366.12, 1366.12, True],
|
||||
["2021-06-30", 20000.0, 21366.12, True],
|
||||
["2022-06-30", 20000.0, 41366.12, False],
|
||||
["2023-06-30", 20000.0, 61366.12, False],
|
||||
["2024-06-30", 20000.0, 81366.12, False],
|
||||
["2025-06-06", 18633.88, 100000.0, False]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
|
||||
@@ -186,83 +186,85 @@ class Asset(AccountsController):
|
||||
if not self.available_for_use_date:
|
||||
return
|
||||
|
||||
for d in self.get('finance_books'):
|
||||
self.validate_asset_finance_books(d)
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
start = self.clear_depreciation_schedule()
|
||||
for finance_book in self.get('finance_books'):
|
||||
self.validate_asset_finance_books(finance_book)
|
||||
|
||||
# value_after_depreciation - current Asset value
|
||||
if self.docstatus == 1 and d.value_after_depreciation:
|
||||
value_after_depreciation = flt(d.value_after_depreciation)
|
||||
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
||||
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
||||
else:
|
||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
|
||||
d.value_after_depreciation = value_after_depreciation
|
||||
finance_book.value_after_depreciation = value_after_depreciation
|
||||
|
||||
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
|
||||
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
|
||||
cint(self.number_of_depreciations_booked)
|
||||
|
||||
has_pro_rata = self.check_is_pro_rata(d)
|
||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||
|
||||
if has_pro_rata:
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
skip_row = False
|
||||
for n in range(start, number_of_pending_depreciations):
|
||||
|
||||
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row: continue
|
||||
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
|
||||
|
||||
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||
schedule_date = add_months(d.depreciation_start_date,
|
||||
n * cint(d.frequency_of_depreciation))
|
||||
schedule_date = add_months(finance_book.depreciation_start_date,
|
||||
n * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
|
||||
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_sale:
|
||||
from_date = self.get_from_date(d.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||
from_date, date_of_sale)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# For first row
|
||||
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
self.available_for_use_date, d.depreciation_start_date)
|
||||
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||
from_date, finance_book.depreciation_start_date)
|
||||
|
||||
# For first depr schedule date will be the start date
|
||||
# so monthly schedule date is calculated by removing month difference between use date and start date
|
||||
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
|
||||
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
|
||||
|
||||
# For last row
|
||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||
if not self.flags.increase_in_asset_life:
|
||||
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
|
||||
self.to_date = add_months(self.available_for_use_date,
|
||||
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
|
||||
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d,
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
|
||||
depreciation_amount, schedule_date, self.to_date)
|
||||
|
||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
||||
depreciation_amount, d.finance_book)
|
||||
depreciation_amount, finance_book.finance_book)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
@@ -273,10 +275,10 @@ class Asset(AccountsController):
|
||||
self.precision("gross_purchase_amount"))
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != d.expected_value_after_useful_life)
|
||||
or value_after_depreciation < d.expected_value_after_useful_life):
|
||||
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
|
||||
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
||||
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
||||
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
@@ -286,7 +288,7 @@ class Asset(AccountsController):
|
||||
# In pro rata case, for first and last depreciation, month range would be different
|
||||
month_range = months \
|
||||
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
||||
else d.frequency_of_depreciation
|
||||
else finance_book.frequency_of_depreciation
|
||||
|
||||
for r in range(month_range):
|
||||
if (has_pro_rata and n == 0):
|
||||
@@ -312,27 +314,52 @@ class Asset(AccountsController):
|
||||
self.append("schedules", {
|
||||
"schedule_date": date,
|
||||
"depreciation_amount": amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
else:
|
||||
self.append("schedules", {
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
|
||||
# used when depreciation schedule needs to be modified due to increase in asset life
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
def clear_depreciation_schedule(self):
|
||||
start = 0
|
||||
for n in range(len(self.schedules)):
|
||||
if not self.schedules[n].journal_entry:
|
||||
del self.schedules[n:]
|
||||
start = n
|
||||
break
|
||||
start = []
|
||||
num_of_depreciations_completed = 0
|
||||
depr_schedule = []
|
||||
|
||||
for schedule in self.get('schedules'):
|
||||
|
||||
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 2):
|
||||
start.append(num_of_depreciations_completed)
|
||||
num_of_depreciations_completed = 0
|
||||
|
||||
# to ensure that start will only be updated once for each FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 1):
|
||||
if schedule.journal_entry:
|
||||
num_of_depreciations_completed += 1
|
||||
depr_schedule.append(schedule)
|
||||
else:
|
||||
start.append(num_of_depreciations_completed)
|
||||
num_of_depreciations_completed = 0
|
||||
|
||||
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
|
||||
if len(start) == (len(self.finance_books) - 1):
|
||||
start.append(num_of_depreciations_completed)
|
||||
|
||||
# when the Depreciation Schedule is being created for the first time
|
||||
if start == []:
|
||||
start = [0] * len(self.finance_books)
|
||||
else:
|
||||
self.schedules = depr_schedule
|
||||
|
||||
return start
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
@@ -349,7 +376,9 @@ class Asset(AccountsController):
|
||||
|
||||
if from_date:
|
||||
return from_date
|
||||
return self.available_for_use_date
|
||||
|
||||
# since depr for available_for_use_date is not yet booked
|
||||
return add_days(self.available_for_use_date, -1)
|
||||
|
||||
# if it returns True, depreciation_amount will not be equal for the first and last rows
|
||||
def check_is_pro_rata(self, row):
|
||||
|
||||
@@ -207,9 +207,9 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
|
||||
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
|
||||
("Debtors - _TC", 25000.0, 0.0)
|
||||
)
|
||||
|
||||
@@ -491,10 +491,10 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 27534.25, 27534.25],
|
||||
["2031-12-31", 30000.0, 57534.25],
|
||||
["2032-12-31", 30000.0, 87534.25],
|
||||
["2033-01-30", 2465.75, 90000.0]
|
||||
['2030-12-31', 27616.44, 27616.44],
|
||||
['2031-12-31', 30000.0, 57616.44],
|
||||
['2032-12-31', 30000.0, 87616.44],
|
||||
['2033-01-30', 2383.56, 90000.0]
|
||||
]
|
||||
|
||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||
@@ -544,10 +544,10 @@ class TestDepreciationMethods(AssetSetup):
|
||||
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 28493.15, 28493.15],
|
||||
["2031-12-31", 35753.43, 64246.58],
|
||||
["2032-12-31", 17876.71, 82123.29],
|
||||
["2033-06-06", 5376.71, 87500.0]
|
||||
['2030-12-31', 28630.14, 28630.14],
|
||||
['2031-12-31', 35684.93, 64315.07],
|
||||
['2032-12-31', 17842.47, 82157.54],
|
||||
['2033-06-06', 5342.46, 87500.0]
|
||||
]
|
||||
|
||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||
@@ -580,10 +580,10 @@ class TestDepreciationMethods(AssetSetup):
|
||||
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 11780.82, 11780.82],
|
||||
["2031-12-31", 44109.59, 55890.41],
|
||||
["2032-12-31", 22054.8, 77945.21],
|
||||
["2033-07-12", 9554.79, 87500.0]
|
||||
["2030-12-31", 11849.32, 11849.32],
|
||||
["2031-12-31", 44075.34, 55924.66],
|
||||
["2032-12-31", 22037.67, 77962.33],
|
||||
["2033-07-12", 9537.67, 87500.0]
|
||||
]
|
||||
|
||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||
@@ -642,7 +642,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
calculate_depreciation = 1,
|
||||
available_for_use_date = getdate("2019-12-31"),
|
||||
available_for_use_date = getdate("2020-01-01"),
|
||||
total_number_of_depreciations = 3,
|
||||
expected_value_after_useful_life = 10000,
|
||||
depreciation_start_date = getdate("2020-07-01"),
|
||||
@@ -653,7 +653,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
["2020-07-01", 15000, 15000],
|
||||
["2021-07-01", 30000, 45000],
|
||||
["2022-07-01", 30000, 75000],
|
||||
["2022-12-31", 15000, 90000]
|
||||
["2023-01-01", 15000, 90000]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@@ -976,6 +976,82 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertEqual(len(asset.schedules), 1)
|
||||
|
||||
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
available_for_use_date = "2019-12-31",
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
asset.calculate_depreciation = 1
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-01-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 6,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-01-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.submit()
|
||||
|
||||
post_depreciation_entries(date="2020-04-01")
|
||||
asset.load_from_db()
|
||||
|
||||
asset.clear_depreciation_schedule()
|
||||
|
||||
self.assertEqual(len(asset.schedules), 6)
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, "1")
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, "2")
|
||||
|
||||
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
available_for_use_date = "2019-12-31",
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
asset.calculate_depreciation = 1
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 6,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.save()
|
||||
|
||||
self.assertEqual(len(asset.schedules), 9)
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, 1)
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, 2)
|
||||
|
||||
def test_depreciation_entry_cancellation(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
|
||||
@@ -256,11 +256,7 @@ class StockController(AccountsController):
|
||||
for d in self.items:
|
||||
if not d.batch_no: continue
|
||||
|
||||
serial_nos = [sr.name for sr in frappe.get_all("Serial No",
|
||||
{'batch_no': d.batch_no, 'status': 'Inactive'})]
|
||||
|
||||
if serial_nos:
|
||||
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
|
||||
frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
|
||||
|
||||
d.batch_no = None
|
||||
d.db_set("batch_no", None)
|
||||
|
||||
@@ -276,10 +276,29 @@ def guess_territory():
|
||||
|
||||
def decorate_quotation_doc(doc):
|
||||
for d in doc.get("items", []):
|
||||
item_code = d.item_code
|
||||
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
|
||||
|
||||
# Variant Item
|
||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||
variant_data = frappe.db.get_values(
|
||||
"Item",
|
||||
filters={"item_code": item_code},
|
||||
fieldname=["variant_of", "item_name", "image"],
|
||||
as_dict=True
|
||||
)[0]
|
||||
item_code = variant_data.variant_of
|
||||
fields = fields[1:]
|
||||
d.web_item_name = variant_data.item_name
|
||||
|
||||
if variant_data.image: # get image from variant or template web item
|
||||
d.thumbnail = variant_data.image
|
||||
fields = fields[2:]
|
||||
|
||||
d.update(frappe.db.get_value(
|
||||
"Website Item",
|
||||
{"item_code": d.item_code},
|
||||
["web_item_name", "thumbnail", "website_image", "description", "route"],
|
||||
{"item_code": item_code},
|
||||
fields,
|
||||
as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -9,8 +9,13 @@ from frappe.utils import add_months, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
from erpnext.e_commerce.shopping_cart.cart import (
|
||||
_get_cart_quotation,
|
||||
get_cart_quotation,
|
||||
get_party,
|
||||
update_cart,
|
||||
)
|
||||
from erpnext.tests.utils import change_settings, create_test_contact_and_address
|
||||
|
||||
|
||||
class TestShoppingCart(unittest.TestCase):
|
||||
@@ -34,6 +39,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
self.disable_shopping_cart()
|
||||
|
||||
@@ -128,6 +134,43 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
self.remove_test_quotation(quotation)
|
||||
|
||||
@change_settings("E Commerce Settings",{
|
||||
"company": "_Test Company",
|
||||
"enabled": 1,
|
||||
"default_customer_group": "_Test Customer Group",
|
||||
"price_list": "_Test Price List India",
|
||||
"show_price": 1
|
||||
})
|
||||
def test_add_item_variant_without_web_item_to_cart(self):
|
||||
"Test adding Variants having no Website Items in cart via Template Web Item."
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
template_item = make_item("Test-Tshirt-Temp", {
|
||||
"has_variant": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
"attributes": [
|
||||
{"attribute": "Test Size"},
|
||||
{"attribute": "Test Colour"}
|
||||
]
|
||||
})
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": "Small", "Test Colour": "Red"
|
||||
})
|
||||
variant.save()
|
||||
make_website_item(template_item) # publish template not variant
|
||||
|
||||
update_cart("Test-Tshirt-Temp-S-R", 1)
|
||||
|
||||
cart = get_cart_quotation() # test if cart page gets data without errors
|
||||
doc = cart.get("doc")
|
||||
|
||||
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
|
||||
|
||||
# test if items are rendered without error
|
||||
frappe.render_template("templates/includes/cart/cart_items.html", cart)
|
||||
|
||||
def create_tax_rule(self):
|
||||
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
||||
try:
|
||||
|
||||
@@ -3,24 +3,26 @@ import unittest
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||
setup_e_commerce_settings,
|
||||
)
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
class TestVariantSelector(unittest.TestCase):
|
||||
class TestVariantSelector(ERPNextTestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.template_item = make_item("Test-Tshirt-Temp", {
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
template_item = make_item("Test-Tshirt-Temp", {
|
||||
"has_variant": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
"attributes": [
|
||||
{
|
||||
"attribute": "Test Size"
|
||||
},
|
||||
{
|
||||
"attribute": "Test Colour"
|
||||
}
|
||||
{"attribute": "Test Size"},
|
||||
{"attribute": "Test Colour"}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -28,19 +30,16 @@ class TestVariantSelector(unittest.TestCase):
|
||||
for size in ("Large", "Medium",):
|
||||
for colour in ("Red", "Green",):
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": size,
|
||||
"Test Colour": colour
|
||||
"Test Size": size, "Test Colour": colour
|
||||
})
|
||||
variant.save()
|
||||
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": "Small",
|
||||
"Test Colour": "Red"
|
||||
"Test Size": "Small", "Test Colour": "Red"
|
||||
})
|
||||
variant.save()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
make_website_item(template_item) # publish template not variants
|
||||
|
||||
def test_item_attributes(self):
|
||||
"""
|
||||
@@ -51,8 +50,6 @@ class TestVariantSelector(unittest.TestCase):
|
||||
"""
|
||||
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
|
||||
|
||||
make_website_item(self.template_item) # publish template not variants
|
||||
|
||||
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||
|
||||
self.assertEqual(attr_data[0]["attribute"], "Test Size")
|
||||
@@ -72,7 +69,7 @@ class TestVariantSelector(unittest.TestCase):
|
||||
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
|
||||
|
||||
# teardown
|
||||
small_variant.disabled = 1
|
||||
small_variant.disabled = 0
|
||||
small_variant.save()
|
||||
|
||||
def test_next_item_variant_values(self):
|
||||
@@ -84,8 +81,6 @@ class TestVariantSelector(unittest.TestCase):
|
||||
There is a ** Small-Red ** Tshirt. No other colour in this size.
|
||||
On selecting ** Small **, only ** Red ** should be selectable next.
|
||||
"""
|
||||
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||
|
||||
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
|
||||
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
|
||||
filtered_items = next_values["filtered_items"]
|
||||
@@ -94,3 +89,31 @@ class TestVariantSelector(unittest.TestCase):
|
||||
self.assertEqual(next_colours.pop(), "Red")
|
||||
self.assertEqual(len(filtered_items), 1)
|
||||
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
|
||||
|
||||
def test_exact_match_with_price(self):
|
||||
"""
|
||||
Test price fetching and matching of variant without Website Item
|
||||
"""
|
||||
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
setup_e_commerce_settings({
|
||||
"company": "_Test Company",
|
||||
"enabled": 1,
|
||||
"default_customer_group": "_Test Customer Group",
|
||||
"price_list": "_Test Price List India",
|
||||
"show_price": 1
|
||||
})
|
||||
|
||||
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
|
||||
next_values = get_next_attribute_and_values(
|
||||
"Test-Tshirt-Temp",
|
||||
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
|
||||
)
|
||||
print(">>>>", next_values)
|
||||
price_info = next_values["product_info"]["price"]
|
||||
|
||||
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||
self.assertEqual(price_info["price_list_rate"], 100.0)
|
||||
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
|
||||
@@ -1,7 +1,12 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
|
||||
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
|
||||
from erpnext.utilities.product import get_price
|
||||
|
||||
|
||||
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
||||
@@ -143,14 +148,13 @@ def get_next_attribute_and_values(item_code, selected_attributes):
|
||||
filtered_items_count = len(filtered_items)
|
||||
|
||||
# get product info if exact match
|
||||
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||
# from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||
if exact_match:
|
||||
data = get_product_info_for_website(exact_match[0])
|
||||
product_info = data.product_info
|
||||
cart_settings = get_shopping_cart_settings()
|
||||
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
|
||||
|
||||
if product_info:
|
||||
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
|
||||
if not data.cart_settings.show_price:
|
||||
product_info = None
|
||||
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
||||
else:
|
||||
product_info = None
|
||||
|
||||
@@ -195,3 +199,20 @@ def get_item_attributes(item_code):
|
||||
|
||||
return attributes
|
||||
|
||||
def get_item_variant_price_dict(item_code, cart_settings):
|
||||
if cart_settings.enabled and cart_settings.show_price:
|
||||
is_guest = frappe.session.user == "Guest"
|
||||
# Show Price if logged in.
|
||||
# If not logged in, check if price is hidden for guest.
|
||||
if not is_guest or not cart_settings.hide_price_for_guest:
|
||||
price_list = _set_price_list(cart_settings, None)
|
||||
price = get_price(
|
||||
item_code,
|
||||
price_list,
|
||||
cart_settings.default_customer_group,
|
||||
cart_settings.company
|
||||
)
|
||||
return {"price": price}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import format_date
|
||||
from frappe.utils.data import add_days, formatdate, today
|
||||
|
||||
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import (
|
||||
@@ -82,6 +83,13 @@ class TestMaintenanceSchedule(unittest.TestCase):
|
||||
|
||||
#checks if visit status is back updated in schedule
|
||||
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
|
||||
self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date))
|
||||
|
||||
#checks if visit status is updated on cancel
|
||||
visit.cancel()
|
||||
ms.reload()
|
||||
self.assertTrue(ms.schedules[1].completion_status, "Pending")
|
||||
self.assertEqual(ms.schedules[1].actual_date, None)
|
||||
|
||||
def test_serial_no_filters(self):
|
||||
# Without serial no. set in schedule -> returns None
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_datetime
|
||||
from frappe.utils import format_date, get_datetime
|
||||
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
@@ -28,20 +28,24 @@ class MaintenanceVisit(TransactionBase):
|
||||
if item_ref:
|
||||
start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
|
||||
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
|
||||
frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date))
|
||||
frappe.throw(_("Date must be between {0} and {1}")
|
||||
.format(format_date(start_date), format_date(end_date)))
|
||||
|
||||
|
||||
def validate(self):
|
||||
self.validate_serial_no()
|
||||
self.validate_maintenance_date()
|
||||
self.validate_purpose_table()
|
||||
|
||||
def update_completion_status(self):
|
||||
def update_status_and_actual_date(self, cancel=False):
|
||||
status = "Pending"
|
||||
actual_date = None
|
||||
if not cancel:
|
||||
status = self.completion_status
|
||||
actual_date = self.mntc_date
|
||||
if self.maintenance_schedule_detail:
|
||||
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status)
|
||||
|
||||
def update_actual_date(self):
|
||||
if self.maintenance_schedule_detail:
|
||||
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
|
||||
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status)
|
||||
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date)
|
||||
|
||||
def update_customer_issue(self, flag):
|
||||
if not self.maintenance_schedule:
|
||||
@@ -102,12 +106,12 @@ class MaintenanceVisit(TransactionBase):
|
||||
def on_submit(self):
|
||||
self.update_customer_issue(1)
|
||||
frappe.db.set(self, 'status', 'Submitted')
|
||||
self.update_completion_status()
|
||||
self.update_actual_date()
|
||||
self.update_status_and_actual_date()
|
||||
|
||||
def on_cancel(self):
|
||||
self.check_if_last_visit()
|
||||
frappe.db.set(self, 'status', 'Cancelled')
|
||||
self.update_status_and_actual_date(cancel=True)
|
||||
|
||||
def on_update(self):
|
||||
pass
|
||||
|
||||
@@ -949,6 +949,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
|
||||
warehouses, item.get("quantity"), company, ignore_validation=True)
|
||||
|
||||
required_qty = item.get("quantity")
|
||||
# get available material by transferring to production warehouse
|
||||
for d in locations:
|
||||
if required_qty <=0: return
|
||||
|
||||
@@ -959,12 +960,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
|
||||
new_dict.update({
|
||||
"quantity": quantity,
|
||||
"material_request_type": "Material Transfer",
|
||||
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
|
||||
"from_warehouse": d.get("warehouse")
|
||||
})
|
||||
|
||||
required_qty -= quantity
|
||||
new_mr_items.append(new_dict)
|
||||
|
||||
# raise purchase request for remaining qty
|
||||
if required_qty:
|
||||
stock_uom, purchase_uom = frappe.db.get_value(
|
||||
'Item',
|
||||
|
||||
@@ -4,6 +4,39 @@
|
||||
|
||||
frappe.query_reports["BOM Operations Time"] = {
|
||||
"filters": [
|
||||
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"label": __("Item Code"),
|
||||
"fieldtype": "Link",
|
||||
"width": "100",
|
||||
"options": "Item",
|
||||
"get_query": () =>{
|
||||
return {
|
||||
filters: { "disabled": 0, "is_stock_item": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_id",
|
||||
"label": __("BOM ID"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"width": "100",
|
||||
"options": "BOM",
|
||||
"get_data": function(txt) {
|
||||
return frappe.db.get_link_options("BOM", txt);
|
||||
},
|
||||
"get_query": () =>{
|
||||
return {
|
||||
filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "workstation",
|
||||
"label": __("Workstation"),
|
||||
"fieldtype": "Link",
|
||||
"width": "100",
|
||||
"options": "Workstation"
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2020-03-03 01:41:20.862521",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "",
|
||||
"modified": "2020-03-03 01:41:20.862521",
|
||||
"modified": "2022-01-20 14:21:47.771591",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operations Time",
|
||||
|
||||
@@ -12,19 +12,15 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
bom_wise_data = {}
|
||||
bom_data, report_data = [], []
|
||||
|
||||
bom_data = []
|
||||
for d in frappe.db.sql("""
|
||||
SELECT
|
||||
bom.name, bom.item, bom.item_name, bom.uom,
|
||||
bomps.operation, bomps.workstation, bomps.time_in_mins
|
||||
FROM `tabBOM` bom, `tabBOM Operation` bomps
|
||||
WHERE
|
||||
bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent
|
||||
""", as_dict=1):
|
||||
bom_operation_data = get_filtered_data(filters)
|
||||
|
||||
for d in bom_operation_data:
|
||||
row = get_args()
|
||||
if d.name not in bom_data:
|
||||
bom_wise_data[d.name] = []
|
||||
bom_data.append(d.name)
|
||||
row.update(d)
|
||||
else:
|
||||
@@ -34,14 +30,49 @@ def get_data(filters):
|
||||
"time_in_mins": d.time_in_mins
|
||||
})
|
||||
|
||||
data.append(row)
|
||||
# maintain BOM wise data for grouping such as:
|
||||
# {"BOM A": [{Row1}, {Row2}], "BOM B": ...}
|
||||
bom_wise_data[d.name].append(row)
|
||||
|
||||
used_as_subassembly_items = get_bom_count(bom_data)
|
||||
|
||||
for d in data:
|
||||
d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0)
|
||||
for d in bom_wise_data:
|
||||
for row in bom_wise_data[d]:
|
||||
row.used_as_subassembly_items = used_as_subassembly_items.get(row.name, 0)
|
||||
report_data.append(row)
|
||||
|
||||
return data
|
||||
return report_data
|
||||
|
||||
def get_filtered_data(filters):
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_ops = frappe.qb.DocType("BOM Operation")
|
||||
|
||||
bom_ops_query = (
|
||||
frappe.qb.from_(bom)
|
||||
.join(bom_ops).on(bom.name == bom_ops.parent)
|
||||
.select(
|
||||
bom.name, bom.item, bom.item_name, bom.uom,
|
||||
bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins
|
||||
).where(
|
||||
(bom.docstatus == 1)
|
||||
& (bom.is_active == 1)
|
||||
)
|
||||
)
|
||||
|
||||
if filters.get("item_code"):
|
||||
bom_ops_query = bom_ops_query.where(bom.item == filters.get("item_code"))
|
||||
|
||||
if filters.get("bom_id"):
|
||||
bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id")))
|
||||
|
||||
if filters.get("workstation"):
|
||||
bom_ops_query = bom_ops_query.where(
|
||||
bom_ops.workstation == filters.get("workstation")
|
||||
)
|
||||
|
||||
bom_operation_data = bom_ops_query.run(as_dict=True)
|
||||
|
||||
return bom_operation_data
|
||||
|
||||
def get_bom_count(bom_data):
|
||||
data = frappe.get_all("BOM Item",
|
||||
@@ -68,13 +99,13 @@ def get_columns(filters):
|
||||
"options": "BOM",
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"width": 140
|
||||
"width": 220
|
||||
}, {
|
||||
"label": _("BOM Item Code"),
|
||||
"label": _("Item Code"),
|
||||
"options": "Item",
|
||||
"fieldname": "item",
|
||||
"fieldtype": "Link",
|
||||
"width": 140
|
||||
"width": 150
|
||||
}, {
|
||||
"label": _("Item Name"),
|
||||
"fieldname": "item_name",
|
||||
@@ -85,13 +116,13 @@ def get_columns(filters):
|
||||
"options": "UOM",
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"width": 140
|
||||
"width": 100
|
||||
}, {
|
||||
"label": _("Operation"),
|
||||
"options": "Operation",
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"width": 120
|
||||
"width": 140
|
||||
}, {
|
||||
"label": _("Workstation"),
|
||||
"options": "Workstation",
|
||||
@@ -101,11 +132,11 @@ def get_columns(filters):
|
||||
}, {
|
||||
"label": _("Time (In Mins)"),
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Int",
|
||||
"width": 140
|
||||
"fieldtype": "Float",
|
||||
"width": 120
|
||||
}, {
|
||||
"label": _("Sub-assembly BOM Count"),
|
||||
"fieldname": "used_as_subassembly_items",
|
||||
"fieldtype": "Int",
|
||||
"width": 180
|
||||
"width": 200
|
||||
}]
|
||||
|
||||
@@ -337,8 +337,11 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
||||
erpnext.patches.v13_0.rename_ksa_qr_field
|
||||
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
|
||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
||||
erpnext.patches.v13_0.update_tax_category_for_rcm
|
||||
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||
erpnext.patches.v13_0.agriculture_deprecation_warning
|
||||
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
||||
erpnext.patches.v13_0.hospitality_deprecation_warning
|
||||
erpnext.patches.v13_0.delete_bank_reconciliation_detail
|
||||
|
||||
13
erpnext/patches/v13_0/delete_bank_reconciliation_detail.py
Normal file
13
erpnext/patches/v13_0/delete_bank_reconciliation_detail.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2019, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \
|
||||
frappe.db.exists('DocType', 'Bank Clearance Detail'):
|
||||
|
||||
frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1)
|
||||
10
erpnext/patches/v13_0/hospitality_deprecation_warning.py
Normal file
10
erpnext/patches/v13_0/hospitality_deprecation_warning.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import click
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
click.secho(
|
||||
"Hospitality Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
|
||||
"When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/hospitality",
|
||||
fg="yellow",
|
||||
)
|
||||
18
erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py
Normal file
18
erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
doctype = "Stock Reconciliation Item"
|
||||
|
||||
if not frappe.db.has_column(doctype, "current_serial_no"):
|
||||
# nothing to fix if column doesn't exist
|
||||
return
|
||||
|
||||
sr_item = frappe.qb.DocType(doctype)
|
||||
|
||||
(frappe.qb
|
||||
.update(sr_item)
|
||||
.set(sr_item.current_serial_no, None)
|
||||
.where(sr_item.current_qty == 0)
|
||||
).run()
|
||||
@@ -5,7 +5,7 @@ import datetime
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, now_datetime, nowdate
|
||||
from frappe.utils import add_months, add_to_date, now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
@@ -151,6 +151,27 @@ class TestTimesheet(unittest.TestCase):
|
||||
settings.ignore_employee_time_overlap = initial_setting
|
||||
settings.save()
|
||||
|
||||
def test_to_time(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
from_time = now_datetime()
|
||||
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = emp
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": from_time,
|
||||
"hours": 2,
|
||||
"company": "_Test Company"
|
||||
}
|
||||
)
|
||||
timesheet.save()
|
||||
|
||||
to_time = timesheet.time_logs[0].to_time
|
||||
self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True))
|
||||
|
||||
|
||||
def make_salary_structure_for_timesheet(employee, company=None):
|
||||
salary_structure_name = "Timesheet Salary Structure Test"
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, getdate, time_diff_in_hours
|
||||
from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
|
||||
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.hr.utils import validate_active_employee
|
||||
@@ -136,10 +136,19 @@ class Timesheet(Document):
|
||||
|
||||
def validate_time_logs(self):
|
||||
for data in self.get('time_logs'):
|
||||
self.set_to_time(data)
|
||||
self.validate_overlap(data)
|
||||
self.set_project(data)
|
||||
self.validate_project(data)
|
||||
|
||||
def set_to_time(self, data):
|
||||
if not (data.from_time and data.hours):
|
||||
return
|
||||
|
||||
_to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
|
||||
if data.to_time != _to_time:
|
||||
data.to_time = _to_time
|
||||
|
||||
def validate_overlap(self, data):
|
||||
settings = frappe.get_single('Projects Settings')
|
||||
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)
|
||||
|
||||
@@ -590,7 +590,6 @@ body.product-page {
|
||||
top: -10px;
|
||||
left: -12px;
|
||||
background: var(--red-600);
|
||||
width: 16px;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
font-size: 10px;
|
||||
|
||||
@@ -142,7 +142,7 @@ class Customer(TransactionBase):
|
||||
self.update_lead_status()
|
||||
|
||||
if self.flags.is_new_doc:
|
||||
self.create_lead_address_contact()
|
||||
self.link_lead_address_and_contact()
|
||||
|
||||
self.update_customer_groups()
|
||||
|
||||
@@ -176,63 +176,25 @@ class Customer(TransactionBase):
|
||||
if self.lead_name:
|
||||
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
|
||||
|
||||
def create_lead_address_contact(self):
|
||||
def link_lead_address_and_contact(self):
|
||||
if self.lead_name:
|
||||
# assign lead address to customer (if already not set)
|
||||
address_names = frappe.get_all('Dynamic Link', filters={
|
||||
"parenttype":"Address",
|
||||
"link_doctype":"Lead",
|
||||
"link_name":self.lead_name
|
||||
}, fields=["parent as name"])
|
||||
# assign lead address and contact to customer (if already not set)
|
||||
linked_contacts_and_addresses = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters=[
|
||||
["parenttype", "in", ["Contact", "Address"]],
|
||||
["link_doctype", "=", "Lead"],
|
||||
["link_name", "=", self.lead_name],
|
||||
],
|
||||
fields=["parent as name", "parenttype as doctype"],
|
||||
)
|
||||
|
||||
for address_name in address_names:
|
||||
address = frappe.get_doc('Address', address_name.get('name'))
|
||||
if not address.has_link('Customer', self.name):
|
||||
address.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||
address.save(ignore_permissions=self.flags.ignore_permissions)
|
||||
for row in linked_contacts_and_addresses:
|
||||
linked_doc = frappe.get_doc(row.doctype, row.name)
|
||||
if not linked_doc.has_link('Customer', self.name):
|
||||
linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
|
||||
|
||||
lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
|
||||
|
||||
if not lead.lead_name:
|
||||
frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name))
|
||||
|
||||
if lead.organization_lead:
|
||||
contact_names = frappe.get_all('Dynamic Link', filters={
|
||||
"parenttype":"Contact",
|
||||
"link_doctype":"Lead",
|
||||
"link_name":self.lead_name
|
||||
}, fields=["parent as name"])
|
||||
|
||||
for contact_name in contact_names:
|
||||
contact = frappe.get_doc('Contact', contact_name.get('name'))
|
||||
if not contact.has_link('Customer', self.name):
|
||||
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||
contact.save(ignore_permissions=self.flags.ignore_permissions)
|
||||
|
||||
else:
|
||||
lead.lead_name = lead.lead_name.lstrip().split(" ")
|
||||
lead.first_name = lead.lead_name[0]
|
||||
lead.last_name = " ".join(lead.lead_name[1:])
|
||||
|
||||
# create contact from lead
|
||||
contact = frappe.new_doc('Contact')
|
||||
contact.first_name = lead.first_name
|
||||
contact.last_name = lead.last_name
|
||||
contact.gender = lead.gender
|
||||
contact.salutation = lead.salutation
|
||||
contact.email_id = lead.email_id
|
||||
contact.phone = lead.phone
|
||||
contact.mobile_no = lead.mobile_no
|
||||
contact.is_primary_contact = 1
|
||||
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||
if lead.email_id:
|
||||
contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1))
|
||||
if lead.mobile_no:
|
||||
contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1))
|
||||
contact.flags.ignore_permissions = self.flags.ignore_permissions
|
||||
contact.autoname()
|
||||
if not frappe.db.exists("Contact", contact.name):
|
||||
contact.insert()
|
||||
|
||||
def validate_name_with_customer_group(self):
|
||||
if frappe.db.exists("Customer Group", self.name):
|
||||
|
||||
@@ -23,19 +23,24 @@ def execute(filters=None):
|
||||
row = []
|
||||
|
||||
outstanding_amt = get_customer_outstanding(d.name, filters.get("company"),
|
||||
ignore_outstanding_sales_order=d.bypass_credit_limit_check_at_sales_order)
|
||||
ignore_outstanding_sales_order=d.bypass_credit_limit_check)
|
||||
|
||||
credit_limit = get_credit_limit(d.name, filters.get("company"))
|
||||
|
||||
bal = flt(credit_limit) - flt(outstanding_amt)
|
||||
|
||||
if customer_naming_type == "Naming Series":
|
||||
row = [d.name, d.customer_name, credit_limit, outstanding_amt, bal,
|
||||
d.bypass_credit_limit_check, d.is_frozen,
|
||||
d.disabled]
|
||||
row = [
|
||||
d.name, d.customer_name, credit_limit,
|
||||
outstanding_amt, bal, d.bypass_credit_limit_check,
|
||||
d.is_frozen, d.disabled
|
||||
]
|
||||
else:
|
||||
row = [d.name, credit_limit, outstanding_amt, bal,
|
||||
d.bypass_credit_limit_check_at_sales_order, d.is_frozen, d.disabled]
|
||||
row = [
|
||||
d.name, credit_limit, outstanding_amt, bal,
|
||||
d.bypass_credit_limit_check, d.is_frozen,
|
||||
d.disabled
|
||||
]
|
||||
|
||||
if credit_limit:
|
||||
data.append(row)
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
|
||||
from erpnext.stock.utils import calculate_mapped_packed_items_return
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@@ -128,8 +129,12 @@ class DeliveryNote(SellingController):
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_with_previous_doc()
|
||||
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
# Keeps mapped packed_items in case product bundle is updated.
|
||||
if self.is_return and self.return_against:
|
||||
calculate_mapped_packed_items_return(self)
|
||||
else:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
|
||||
if self._action != 'submit' and not self.is_return:
|
||||
set_batch_nos(self, 'warehouse', throw=True)
|
||||
|
||||
@@ -386,8 +386,7 @@ class TestDeliveryNote(ERPNextTestCase):
|
||||
self.assertEqual(actual_qty, 25)
|
||||
|
||||
# return bundled item
|
||||
dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1,
|
||||
return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
|
||||
dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2)
|
||||
|
||||
# qty after return
|
||||
actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1")
|
||||
@@ -823,6 +822,15 @@ class TestDeliveryNote(ERPNextTestCase):
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def create_return_delivery_note(**args):
|
||||
args = frappe._dict(args)
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
doc = make_return_doc("Delivery Note", args.source_name, None)
|
||||
doc.items[0].rate = args.rate
|
||||
doc.items[0].qty = args.qty
|
||||
doc.submit()
|
||||
return doc
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -347,14 +347,6 @@ class Item(Document):
|
||||
frappe.throw(_("Barcode {0} is not a valid {1} code").format(
|
||||
item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode)
|
||||
|
||||
if item_barcode.barcode != item_barcode.name:
|
||||
# if barcode is getting updated , the row name has to reset.
|
||||
# Delete previous old row doc and re-enter row as if new to reset name in db.
|
||||
item_barcode.set("__islocal", True)
|
||||
item_barcode_entry_name = item_barcode.name
|
||||
item_barcode.name = None
|
||||
frappe.delete_doc("Item Barcode", item_barcode_entry_name)
|
||||
|
||||
def validate_warehouse_for_reorder(self):
|
||||
'''Validate Reorder level table for duplicate and conditional mandatory'''
|
||||
warehouse = []
|
||||
|
||||
@@ -5,7 +5,10 @@ import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
|
||||
create_delivery_note,
|
||||
create_return_delivery_note,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
create_landed_cost_voucher,
|
||||
@@ -232,8 +235,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
|
||||
self.assertEqual(outgoing_rate, 100)
|
||||
|
||||
# Return Entry: Qty = -2, Rate = 150
|
||||
return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
|
||||
company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
|
||||
return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2)
|
||||
|
||||
# check incoming rate for Return entry
|
||||
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, nowdate, nowtime, random_string
|
||||
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
|
||||
|
||||
from erpnext.accounts.utils import get_stock_and_account_balance
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
@@ -439,8 +439,8 @@ class TestStockReconciliation(ERPNextTestCase):
|
||||
self.assertRaises(frappe.ValidationError, sr.submit)
|
||||
|
||||
def test_serial_no_cancellation(self):
|
||||
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
|
||||
if not item.has_serial_no:
|
||||
item.has_serial_no = 1
|
||||
@@ -466,6 +466,31 @@ class TestStockReconciliation(ERPNextTestCase):
|
||||
self.assertEqual(len(active_sr_no), 10)
|
||||
|
||||
|
||||
def test_serial_no_creation_and_inactivation(self):
|
||||
item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1)
|
||||
if not item.has_serial_no:
|
||||
item.has_serial_no = 1
|
||||
item.save()
|
||||
|
||||
item_code = item.name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse,
|
||||
serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100)
|
||||
sr.save()
|
||||
self.assertEqual(cstr(sr.items[0].current_serial_no), "")
|
||||
sr.submit()
|
||||
|
||||
active_sr_no = frappe.get_all("Serial No",
|
||||
filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
|
||||
self.assertEqual(len(active_sr_no), 1)
|
||||
|
||||
sr.cancel()
|
||||
active_sr_no = frappe.get_all("Serial No",
|
||||
filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
|
||||
self.assertEqual(len(active_sr_no), 0)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
if not batch_item_doc.has_batch_no:
|
||||
|
||||
@@ -104,7 +104,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
|
||||
serial_nos = get_serial_nos_data_after_transactions(args)
|
||||
|
||||
return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
|
||||
if last_entry else (0.0, 0.0, 0.0))
|
||||
if last_entry else (0.0, 0.0, None))
|
||||
else:
|
||||
return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
|
||||
else:
|
||||
@@ -420,6 +420,19 @@ def is_reposting_item_valuation_in_progress():
|
||||
if reposting_in_progress:
|
||||
frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
|
||||
|
||||
|
||||
def calculate_mapped_packed_items_return(return_doc):
|
||||
parent_items = set([item.parent_item for item in return_doc.packed_items])
|
||||
against_doc = frappe.get_doc(return_doc.doctype, return_doc.return_against)
|
||||
|
||||
for original_bundle, returned_bundle in zip(against_doc.items, return_doc.items):
|
||||
if original_bundle.item_code in parent_items:
|
||||
for returned_packed_item, original_packed_item in zip(return_doc.packed_items, against_doc.packed_items):
|
||||
if returned_packed_item.parent_item == original_bundle.item_code:
|
||||
returned_packed_item.parent_detail_docname = returned_bundle.name
|
||||
returned_packed_item.qty = (original_packed_item.qty / original_bundle.qty) * returned_bundle.qty
|
||||
|
||||
|
||||
def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
|
||||
"""Check if there are pending reposting job till the specified posting date."""
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ class ItemConfigure {
|
||||
? `<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
|
||||
<div><div>
|
||||
${one_item}
|
||||
${product_info && product_info.price && !$.isEmptyObject()
|
||||
${product_info && product_info.price && !$.isEmptyObject(product_info.price)
|
||||
? '(' + product_info.price.formatted_price_sales_uom + ')'
|
||||
: ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user